以下是「第一章:编译概观」的读书笔记
编译器的前端处理
假设句子为 Compilers are engineered objects.,那么 词法分析器 的工作就是将其转换为 单词流:
[
['名词', 'Compilers'],
['动词', 'are'],
['形容词', 'engineeded'],
['名词', 'objects'],
['结束标记', '.']
]
假设英语的 语法规则 为
规则1 句子 => 主语 动词 宾语 结束标记
规则2 主语 => 名词
规则3 主语 => 定语 名词
规则4 宾语 => 名词
规则5 宾语 => 定语 名词
规则6 定语 => 形容词
...
我们可以根据 语法规则 来 推导 derivation 出一个句子的具体组成
规则
句子
1 主语 动词 宾语 结束标记
2 名词 动词 宾语 结束标记
5 名词 动词 定语 名词 结束标记
6 名词 动词 形容词 名词 结束标记
上面推导证明句子 Compilers are engineered objects. 属于由规则1到6描述出的语言。
自动查找推导的过程叫做解析 或 语法分析 或 parsing。
因此,语法分析器 的工作就是判断输入的单词流是不是源语言的一个句子。
但语法正确的句子可能是无意义的。例如
Rocks are green vegetables.
石头是绿色的蔬菜。
在编程语言中,以 a = a * 2 * b * c * d 为例,如果 b 和 c 都是字符串,那么虽然这句话符合语法,但显然是没有意义的。
因此编译器还需要检查句子的 语义。
编译器前端处理的最后一个任务是生成代码的 中间表示 Intermediate Representations。有些 IT 将程序表示为图,有些则将程序表示为类似于汇编的代码。
比如我们可以把 a = a * 2 * b * c * d 表示为
t0 <- a * 2
t1 <- t0 * b
t2 <- t1 * c
t3 <- t2 * d
a <- t3
编译器需要确定源语言中的每一种结构对应的 IR 形式,我们将在后面的章节中探讨。
优化器
假设源代码为
b <- ...
c <- ...
a <- 1
for i = 1 to n
read d
a <- a * 2 * b * c * d
end
如果优化器发现 2、b、c 的值在每次循环中是不变的,就可以重写代码:
b <- ...
c <- ...
a <- 1
t <- 2 * b * c
for i = 1 to n
read d
a <- a * t * d
end
这样,乘法操作的数目就从 4n 下降到 2n + 2 次了。
大多数优化都包括分析和转换两个过程,我们将在后面的章节中探讨。
后端
编译器的后端会遍历 IR 并针对目标机器输出代码。在此期间,编译器做的工作有:
指令选择
指令选择是将每个 IR 操作在各自的上下文中映射为一个或多个目标机操作。以 a <- a * 2 * b * c * d 为例,它对应的指令为:
如果编译器认为加法比乘法快,也可能会选择使用加法指令。
寄存器分配
在指令选择期间,编译器有意忽略了目标机器的寄存器数目有限这个事实。比如上述代码就用到了 ra、rb、rc、rd、r2 这 5 个寄存器,实际上可以将代码重写为如下形式以最小化寄存器的使用:
可以看到,优化后的代码只用到了 r1 和 r2 两个寄存器。
指令调度
为了产生执行更快的代码,代码生成器可能会对代码进行重新排列。 后面章节会讲到。
各组件交互
这个期间的问题较为复杂,后面讲。