赞
踩
编译本质上就是吧源代码翻译成目标代码的过程
编译分为6个阶段
词法分析就是把字符串
转化为Token
的一个过程
目的是让编译器对这对Token进行语法分析,检查是否有语法错误,分析这段代码的语法结构
采用AST
(抽象语法树)的方式来实现将代码划分出层次清晰的语法结构
叶子节点,就是词法分析阶段生成的 Token(图中带边框的节点)。对这棵 AST 做深度优先的遍历,你就能依次得到原来的 Token。
生成 AST 以后,程序的语法结构就很清晰了
结构的问题解决了,接下来就是语义的问题了
语义分析分为语义理解和语义检查;
语义理解就是让程序理解程序中表达式的含义;
语义检查就是对句子进行检查,排除语法正确但语义有问题的句子
比如说 a+3 这个表达式的含义是,获取变量a的值与3相加再返回。这个表达式我们是很容易理解但编译器不理解啊,所有我们需要给AST每个阶段附加语义规则
让编译器去理解,比如:
这样的话,如果你深度遍历 AST,并执行每个节点附带的语义规则,就可以得到 a+3 的值。这意味着,我们正确地理解了这个表达式的含义。运用相同的方法,我们也就能够理解一个句子的含义、一个函数的含义,乃至整段源代码的含义。
但是这样还是不够,还要考虑两个问题:
一、变量的作用域问题
- int a = 10; //全局变量
- int foo(int a){ //参数里有另一个变量a
- int b = a + 3; //这里的a指的是哪一个?
- return b;
- }
这里有两个变量名为a,显然此时我们应该采用就近原则选择参数a而不是全局变量a
为实现这种效果我引入了一个引用消解
的概念
引用消解需要在上下文中查找某个标识符的定义与引用的关系,所以语义分析
的重要特点,就是做上下文相关的分析
。
二、变量的数据类型问题
在语义分析阶段,编译器还会识别出数据的类型
。比如,在计算“a+3”的时候,我们必须知道 a 和 3 的类型是什么。因为即使同样是加法运算,对于整型和浮点型数据,其计算方法也是不一样的。
语义分析获得的一些信息(引用消解信息、类型信息等),会附加到 AST 上。这样的 AST 叫做带有标注信息的 AST
(Annotated AST/Decorated AST),用于更全面地反映源代码的含义。
好了,前面我所说的,都是如何让编译器更好地理解程序的语义。不过在语义分析阶段,编译器还要做很多语义方面的检查工作。
在自然语言里,我们可以很容易写出一个句子,它在语法上是正确的,但语义上是错误的。比如,“小猫喝水”这句话,它在语法和语义上都是对的;而“水喝小猫”这句话,语法是对的,语义上则是不对的。
计算机程序也会存在很多类似的语义错误的情况。比如说,对于“int b = a+3”的这个语句,语义规则要求,等号右边的表达式必须返回一个整型的数据(或者能够自动转换成整型的数据),否则就跟变量 b 的类型不兼容。如果右边的表达式“a+3”的计算结果是浮点型的,就违背了语义规则,就要报错。
中间代码(IR),是处于源代码和目标代码之间的一种表示形式。
我们倾向于使用 IR 有两个原因。
在生成目标代码之前,需要做的优化工作可以有很多,这通常也是编译器在运行时,花费时间最长的一个部分。
那为什么需要做优化工作呢?这里又有两大类的原因。
- Class Person{
- private String name;
- public String getName(){
- return name;
- }
- public void setName(String newName){
- this.name = newName
- }
- }
如果你在程序里用“person.getName()”来获取 Person 的 name 字段,会是一个开销很大的操作,因为它涉及函数调用。
怎样简化呢?就是跳过方法的调用。我们直接根据对象的地址计算出 name 属性的地址,然后直接从内存取值就行。这样优化之后,性能会提高好多倍。这种优化方法就叫做内联
(inlining),也就是把原来程序中的函数调用去掉,把函数内的逻辑直接嵌入函数调用者的代码中。
总结起来,我们在把源代码翻译成目标代码的过程中,没有必要“直译”,而是可以“意译”。这样我们完成相同的工作,对资源的消耗会更少。
- int bar(){
- int a = 10*10; //这里在编译时可以直接计算出100这个值,这叫做“常数折叠”
- int b = 20; //这个变量没有用到,可以在代码中删除,这叫做“死代码删除”
-
-
- if (a>0){ //因为a一定大于0,所以判断条件和else语句都可以去掉
- return a+1; //这里可以在编译器就计算出是101
- }
- else{
- return a-1;
- }
- }
- int a = bar(); //这里可以直接换成 a=101
整个对 bar() 函数的调用,可以省略,因为 bar() 的值一定是 101。这些优化工作都可以在编译期间完成。
编译器最后一个阶段的工作,是生成高效率的目标代码,也就是汇编代码。这个阶段,编译器也有几个重要的工作。
寄存器
的分配,让频繁访问的变量(比如循环变量)放到寄存器里,因为访问寄存器要比访问内存快 100 倍左右。重新排序
,从而充分运用 CPU 内部的多个功能部件的并行计算能力。目标代码生成以后,整个编译过程就完成了。Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。