《编译器设计》笔记1:编译概观

·  阅读 147

以下是「第一章:编译概观」的读书笔记

编译器的前端处理

假设句子为 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 为例,它对应的指令为:

c7fabc90083c3642d7ec27d4221e1ef.jpg

如果编译器认为加法比乘法快,也可能会选择使用加法指令。

寄存器分配

在指令选择期间,编译器有意忽略了目标机器的寄存器数目有限这个事实。比如上述代码就用到了 ra、rb、rc、rd、r2 这 5 个寄存器,实际上可以将代码重写为如下形式以最小化寄存器的使用:

c5336a9eaef405bb18626d52c49c5f8.jpg

可以看到,优化后的代码只用到了 r1 和 r2 两个寄存器。

指令调度

为了产生执行更快的代码,代码生成器可能会对代码进行重新排列。 后面章节会讲到。

各组件交互

这个期间的问题较为复杂,后面讲。

分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改