摘要
深入剖析现代编译器运用的算法和技术
强调代码优化和代码生成
体现编译原理教学的理念
编译器设计(第2版)[Engineering a Compiler,Second] 内容简介
1952年一个编译器诞生,至今已经过去了半个多世纪,编译器的发展日臻成熟,关于编译器设计的著作也出版了不少,但既关注设计细节,又具备大局观的经典之作凤毛麟角,《编译器设计(第2版)》即是这样一本难得的佳作。
两位作者多年来一直奋战在研发和教学一线,理论和实践上的丰厚经验都凝结在了《编译器设计(第2版)》中。书中论述了一系列构建现代编译器必需的核心技术,分析了编译器设计者需要面对的诸多问题,阐释了解决这些问题所用到的一些知识点。第2版是时隔8年之后全新修订的版本,充分展现了编译器构造技术的进展。作者重写了书中全部示例,并特别改进了阐述顺序,使得章与章之间的内容更具连续性,也更适合专业人士将这本高校教材作为参考书。
编译器设计(第2版)[Engineering a Compiler,Second] 目录
第1章 编译概观
1.1 简介
1.2 编译器结构
1.3 转换概述
1.3.1 前端
1.3.2 优化器
1.3.3 后端
1.4 小结和展望
第2章 词法分析器
2.1 简介
2.2 识别单词
2.2.1 识别器的形式化
2.2.2 识别更复杂的单词
2.3 正则表达式
2.3.1 符号表示法的形式化
2.3.2 示例
2.3.3 RE的闭包性质
2.4 从正则表达式到词法分析器
2.4.1 非确定性有限自动机
2.4.2 从正则表达式到NFA:Thompson构造法
2.4.3 从NFA到DFA:子集构造法
2.4.4 从DFA到最小DFA:Hopcroft算法
2.4.5 将DFA用做识别器
2.5 实现词法分析器
2.5.1 表驱动词法分析器
2.5.2 直接编码的词法分析器
2.5.3 手工编码的词法分析器
2.5.4 处理关键字
2.6 高级主题
2.6.1 从DFA到正则表达式
2.6.2 DFA最小化的另一种方法:Brzozowski算法
2.6.3 无闭包的正则表达式
2.7 小结和展望
第3章 语法分析器
3.1 简介
3.2 语法的表示
3.2.1 为什么不使用正则表达式
3.2.2 上下文无关语法
3.2.3 更复杂的例子
3.2.4 将语义编码到结构中
3.2.5 为输入符号串找到推导
3.3 自顶向下语法分析
3.3.1 为进行自顶向下语法分析而转换语法
3.3.2 自顶向下的递归下降语法分析器
3.3.3 表驱动的LL(1)语法分析器
3.4 自底向上语法分析
3.4.1 LR(1)语法分析算法
3.4.2 构建LR(1)表
3.4.3 表构造过程中的错误
3.5 实际问题
3.5.1 出错恢复
3.5.2 一元运算符
3.5.3 处理上下文相关的二义性
3.5.4 左递归与右递归
3.6 高级主题
3.6.1 优化语法
3.6.2 减小LR(1)表的规模
3.7 小结和展望
第4章 上下文相关分析
4.1 简介
4.2 类型系统简介
4.2.1 类型系统的目标
4.2.2 类型系统的组件
4.3 属性语法框架
4.3.1 求值的方法
4.3.2 环
4.3.3 扩展实例
4.3.4 属性语法方法的问题
4.4 特设语法制导转换
4.4.1 特设语法制导转换的实现
4.4.2 例子
4.5 高级主题
4.5.1 类型推断中更困难的问题
4.5.2 改变结合性
4.6 小结和展望
第5章 中间表示
5.1 简介
5.2 图IR
5.2.1 与语法相关的树
5.2.2 图
5.3 线性IR
5.3.1 堆栈机代码
5.3.2 三地址代码
5.3.3 线性代码的表示
5.3.4 根据线性代码建立控制流图
5.4 将值映射到名字
5.4.1 临时值的命名
5.4.2 静态单赋值形式
5.4.3 内存模型
5.5 符号表
5.5.1 散列表
5.5.2 建立符号表
5.5.3 处理嵌套的作用域
5.5.4 符号表的许多用途
5.5.5 符号表技术的其他用途
5.6 小结和展望
第6章 过程抽象
6.1 简介
6.2 过程调用
6.3 命名空间
6.3.1 类Algol语言的命名空间
6.3.2 用于支持类Algol语言的运行时结构
6.3.3 面向对象语言的命名空间
6.3.4 支持面向对象语言的运行时结构
6.4 过程之间值的传递
6.4.1 传递参数
6.4.2 返回值
6.4.3 确定可寻址性
6.5 标准化链接
6.6 高级主题
6.6.1 堆的显式管理
6.6.2 隐式释放
6.7 小结和展望
第7章 代码形式
7.1 简介
7.2 分配存储位置
7.2.1 设定运行时数据结构的位置
7.2.2 数据区的布局
7.2.3 将值保持在寄存器中
7.3 算术运算符
7.3.1 减少对寄存器的需求
7.3.2 访问参数值
7.3.3 表达式中的函数调用
7.3.4 其他算术运算符
7.3.5 混合类型表达式
7.3.6 作为运算符的赋值操作
7.4 布尔运算符和关系运算符
7.4.1 表示
7.4.2 对关系操作的硬件支持
7.5 数组的存储和访问
7.5.1 引用向量元素
7.5.2 数组存储布局
7.5.3 引用数组元素
7.5.4 范围检查
7.6 字符串
7.6.1 字符串表示
7.6.2 字符串赋值
7.6.3 字符串连接
7.6.4 字符串长度
7.7 结构引用
7.7.1 理解结构布局
7.7.2 结构数组
7.7.3 联合和运行时标记
7.7.4 指针和匿名值
7.8 控制流结构
7.8.1 条件执行
7.8.2 循环和迭代
7.8.3 case语句
7.9 过程调用
7.9.1 实参求值
7.9.2 保存和恢复寄存器
7.10 小结和展望
第8章 优化简介
8.1 简介
8.2 背景
8.2.1 例子
8.2.2 对优化的考虑
8.2.3 优化的时机
8.3 优化的范围
8.4 局部优化
8.4.1 局部值编号
8.4.2 树高平衡
8.5 区域优化
8.5.1 超局部值编号
8.5.2 循环展开
8.6 全局优化
8.6.1 利用活动信息查找未初始化变量
8.6.2 全局代码置放
8.7 过程间优化
8.7.1 内联替换
8.7.2 过程置放
8.7.3 针对过程间优化的编译器组织结构
8.8 小结和展望
第9章 数据流分析
9.1 简介
9.2 迭代数据流分析
9.2.1 支配性
9.2.2 活动变量分析
9.2.3 数据流分析的局限性
9.2.4 其他数据流问题
9.3 静态单赋值形式
9.3.1 构造静态单赋值形式的简单方法
9.3.2 支配边界
9.3.3 放置 函数
9.3.4 重命名
9.3.5 从静态单赋值形式到其他形式的转换
9.3.6 使用静态单赋值形式
9.4 过程间分析
9.4.1 构建调用图
9.4.2 过程间常量传播
9.5 高级主题
9.5.1 结构性数据流算法和可归约性
9.5.2 加速计算支配性的迭代框架算法的执行
9.6 小结和展望
第10章 标量优化
10.1 简介
10.2 消除无用和不可达代码
10.2.1 消除无用代码
10.2.2 消除无用控制流
10.2.3 消除不可达代码
10.3 代码移动
10.3.1 缓式代码移动
10.3.2 代码提升
10.4 特化
10.4.1 尾调用优化
10.4.2 叶调用优化
10.4.3 参数提升
10.5 冗余消除
10.5.1 值相同与名字相同
10.5.2 基于支配者的值编号算法
10.6 为其他变换制造时机
10.6.1 超级块复制
10.6.2 过程复制
10.6.3 循环外提
10.6.4 重命名
10.7 高级主题
10.7.1 合并优化
10.7.2 强度削减
10.7.3 选择一种优化序列
10.8 小结和展望
第11章 指令选择
11.1 简介
11.2 代码生成
11.3 扩展简单的树遍历方案
11.4 通过树模式匹配进行指令选择
11.4.1 重写规则
11.4.2 找到平铺方案
11.4.3 工具
11.5 通过窥孔优化进行指令选择
11.5.1 窥孔优化
11.5.2 窥孔变换程序
11.6 高级主题
11.6.1 学习窥孔模式
11.6.2 生成指令序列
11.7 小结和展望
第12章 指令调度
12.1 简介
12.2 指令调度问题
12.2.1 度量调度质量的其他方式
12.2.2 是什么使调度这样难
12.3 局部表调度
12.3.1 算法
12.3.2 调度具有可变延迟的操作
12.3.3 扩展算法
12.3.4 在表调度算法中打破平局
12.3.5 前向表调度与后向表调度
12.3.6 提高表调度的效率
12.4 区域性调度
12.4.1 调度扩展基本程序块
12.4.2 跟踪调度
12.4.3 通过复制构建适当的上下文环境
12.5 高级主题
12.5.1 软件流水线的策略
12.5.2 用于实现软件流水线的算法
12.6 小结和展望
第13章 寄存器分配
13.1 简介
13.2 背景问题
13.2.1 内存与寄存器
13.2.2 分配与指派
13.2.3 寄存器类别
13.3 局部寄存器分配和指派
13.3.1 自顶向下的局部寄存器分配
13.3.2 自底向上的局部寄存器分配
13.3.3 超越单个程序块
13.4 全局寄存器分配和指派
13.4.1 找到全局活动范围
13.4.2 估算全局逐出代价
13.4.3 冲突和冲突图
13.4.4 自顶向下着色
13.4.5 自底向上着色
13.4.6 合并副本以减小度数
13.4.7 比较自顶向下和自底向上全局分配器
13.4.8 将机器的约束条件编码到冲突图中
13.5 高级主题
13.5.1 图着色寄存器分配方法的变体
13.5.2 静态单赋值形式上的全局寄存器分配
13.6 小结和展望
附录A ILOC
附录B 数据结构
参考文献
索引
编译器设计(第2版)[Engineering a Compiler,Second] 精彩文摘
本书提供了两种视角:编译器构建领域中各问题的整体图景,以及各种可选算法方案的详细讨论。在构思本书的过程中,我们专注于该书的可用性,使其既可作为教科书,又可用做专业人士的参考书。为此,我们特别进行了下述改动。
改进了阐述思想的流程,以帮助按顺序阅读本书的学生。每章章首简介会解释该章的目的,列出主要的概念,并概述主题相关内容。书中的示例已经重写过,使得章与章之间的内容具有连续性。此外,每章都从摘要和一组关键词开始,以帮助那些会将本书用做参考书的读者。
在每节末尾都增加了本节回顾和复习题。复习题用于快速检查读者是否理解了该节的要点。
关键术语的定义放在了它们被首次定义和讨论的段落之后。
大量修订了有关优化的内容,使其能够更广泛地涵盖优化编译器的各种可能性。
现在的编译器开发专注于优化和代码生成。对于新雇用的编译器编写者来说,他们往往会被指派去将代码生成器移植到新处理器,或去修改优化趟,而不会去编写词法分析器或语法分析器。成功的编译器编写者必须熟悉优化(如静态单赋值形式的构建)和代码生成领域当前最好的实践技术(如软件流水线)。他们还必须拥有相关的背景和洞察力,能理解未来可能出现的新技术。最后,他们必须深刻理解词法分析、语法分析和语义推敲(semantic elaboration)技术,能构建或修改编译器前端。
本书是一本教科书、一门教程,帮助学生接触到现代编译器领域中的各种关键问题,并向学生提供解决这些问题所需的背景知识。从第1版开始,我们就维持了各主题之间的基本均衡。前端是实用组件,可以从可靠的厂商购买或由某个开源系统改编而得。但是,优化器和代码生成器通常是对特定处理器定制的,有时甚至针对单个处理器型号定制,因为性能严重依赖于所生成代码的底层细节。这些事实影响到了当今构建编译器的方法,它们也应该影响我们讲授编译器构建课程的方法。
本书结构
本书内容划分为篇幅大致相等的四个部分。
第一部分(第2章~第4章)涵盖编译器前端及建立前端所用工具的设计和构建。
第二部分(第5章~第7章)探讨从源代码到编译器的中间形式的映射,这些章考查前端为优化器和后端所生成代码的种类。
第三部分(第8章~第10章)介绍代码优化。第8章提供对优化的概述。第9章和第10章包含了对分析和转换的更深入的处理,本科课程通常略去这两章。
第四部分(第11章~第13章)专注于编译器的后端所使用的算法。
编译的艺术性与科学性
编译器构建的内容有两部分,一是将理论应用到实践方面所取得的惊人成就,一是对我们能力受限之处的探讨。这些成就包括:现代词法分析器是通过应用正则语言的理论自动构建识别器而建立的;LR语法分析器使用同样的技术执行句柄识别,进而驱动了一个移进归约语法分析器;数据流分析巧妙有效地将格理论应用到程序分析中;代码生成中使用的近似算法为许多真正困难的问题提供了较好的解。
另一方面,编译器构建也揭示了一些难以解决的复杂问题。用于现代处理器的编译器后端对两个以上的NP完全问题(指令调度、寄存器分配,也许还包括指令和数据安排)采用了近似算法来获取答案。这些NP完全问题,虽然看起来与诸如表达式的代数重新关联这种问题相近(示例见图7-1)。但后者有着大量的解决方案,更糟的是,对于这些NP完全问题来说,所要的解往往取决于编译器和应用程序代码中的上下文信息。在编译器对此类问题近似求解时,会面临编译时间和可用内存上的限制。好的编译器会巧妙地混合理论、实践知识、技术和经验。
打开一个现代优化编译器,你会发现各式各样的技术。编译器使用贪婪启发式搜索来探索很大的解空间,使用确定性有限自动机来识别输入中的单词。不动点算法被用于推断程序行为,通过定理证明程序和代数化简器来预测表达式的值。编译器利用快速模式匹配算法将抽象计算映射到机器层次上的操作。它们使用线性丢番图方程和普瑞斯伯格算术(Pressburger arithmetic)来分析数组下标。最后,编译器使用了大量经典的算法和数据结构,如散列表、图算法和稀疏集实现方法等。
本书尝试同时阐释编译器构建的艺术和科学这两方面内容。通过选取足够广泛的题材,向读者表明,确实存在一些折中的解决方案,而设计决策的影响可能是微妙而深远的。另一方面,本书也省去了某些长期以来都列入本科编译器构建课程的技术,随着市场、语言和编译器技术或工具可用性方面的改变,这些技术已变得不那么重要了。
讲述方法
编译器构建是一种工程设计实践。每个方案的成本、优点和复杂程度各异,编译器编写者必须在多种备选方案中做出抉择。每个决策都会影响到最终的编译器。最终产品的质量,取决于抉择过程中所做的每一个理性决断。
因而,对于编译器中的许多设计决策来说,并不存在唯一的正确答案。即使在“理解透彻”和“已解决”的问题中,设计和实现中的细微差别都会影响到编译器的行为及其产生的代码的质量。每个决策都涉及许多方面。举例来说,中间表示的选择对于编译器中其余部分有着深刻的影响,无论是时间和空间需求,还是应用不同算法的难易程度。但实际上确定该决策时,通常可供设计者考虑的时间并不多。第5章考察了中间表示的空间需求,以及其他一些应该在选择中间表示时考虑的问题。在本书中其他地方,我们会再次提出该问题,既会在正文中直接提出,也会在习题里间接提出。
本书探索了编译器的设计空间,既从深度上阐释问题,也从广度上探讨可能的答案。它给出了这些问题的某些解决方法,并说明了使用这些方案的约束条件。编译器编写者需要理解这些问题及其答案,以及所作决策对编译器设计的其他方面的影响。只有这样,编写者才能作出理性和明智的选择。
思想观念
本书阐释了我们在构建编译器方面的思想观念,这是在各自超过25年的研究、授课和实践过程中发展起来的。例如,中间表示应该展示最终代码所关注的那些细节,这种理念导致了对底层表示的偏爱。又比如,值应该驻留在寄存器中,直至分配器发现无法继续保留它为止,这种做法产生了使用虚拟寄存器的例子,以及仅在无可避免时才将值存储到内存的例子。每个编译器都应该包括优化,它简化了编译器其余的部分。多年以来的经验,使得我们能够理性地选择书的主题和展现方式。
关于编程习题的选择
编译器构建方面的课程,提供了在一个具体应用程序(编译器)环境中探索软件设计问题的机会,任何具备编译器构建课程背景的学生,都已经透彻理解了该应用程序的基本功能。在多数编译器设计课程中,编程习题发挥了很大的作用。
我们以这样的方式讲授过这门课:学生从头到尾构建一个简单的编译器,从生成的词法分析器和语法分析器开始,结束于针对某个简化的RISC指令集的代码生成器。我们也以另一种方式讲授过这门课程,学生编写程序来解决各个良好自包含的问题,诸如寄存器分配或指令调度。编程习题的选择实际上非常依赖于本课程在相关课程中所扮演的角色。
在某些学校,编译器课程充当高年级的顶级课程,将来自许多其他课程的概念汇集到一个大型的实际设计和实现项目中。在这样的课程中,学生应该为一门简单的语言编写一个完整的编译器,或者修改一个开源的编译器,以支持新的语言或体系结构特性。这门课程可以按本书的内容组织,从头到尾讲授本书的内容。
在另外一些学校,编译器设计出现在其他课程中,或以其他方式呈现在教学中。此时,编译器设计教师应该专注于算法及其实现,比如局部寄存器分配器或树高重新平衡趟这样的编程习题。在这种情况下,可以选择性讲解本书中的内容,也可以调整讲述的顺序,以满足编程习题的需求。例如,在莱斯大学,我们通常使用简单的局部寄存器分配器作为第一个编程习题,任何具有汇编语言编程经验的学生,都可以理解该问题的基本要素。但这种策略,需要让学生在学习第2章之前,首先接触第13章的内容。
不管采用哪种方案,本课程都应该从其他课程取材。在计算机组织结构、汇编语言编程、操作系统、计算机体系结构、算法和形式语言之间,存在着明显的关联。尽管编译器构建与其他课程的关联不那么明显,但这种关联同等重要。第7章中讨论的字符复制,对于网络协议、文件服务器和Web服务器等应用程序的性能而言,都发挥着关键的作用。第2章中用于词法分析的技术可以应用到文本编辑和URL过滤等领域。第13章中自底向上的局部寄存器分配器是最优离线页面替换算法MIN的“近亲”。
本文来自怪你过分美丽投稿,不代表电子书资源网立场,如若转载,请联系原作者获取。