当前位置:   article > 正文

编译原理概述

编译原理

概述

编译本质上就是吧源代码翻译成目标代码的过程

编译分为6个阶段

  1. 词法分析
  2. 语法分析
  3. 语义分析
  4. 生成中间代码
  5. 优化
  6. 生成目标代码

在这里插入图片描述

词法分析(Lexical Analysis)

词法分析就是把字符串转化为Token的一个过程

  • Token:就是单词的意思,程序里面的单词叫Token,它可以分成关键字、标识符、字面量、操作符号等多个种类。
  • 字符串:程序本质上就一个字符串

在这里插入图片描述

语法分析(Syntactic Analysis)

目的是让编译器对这对Token进行语法分析,检查是否有语法错误,分析这段代码的语法结构
采用AST(抽象语法树)的方式来实现将代码划分出层次清晰的语法结构

  • AST:体现语法规则的、树状的数据结构

在这里插入图片描述

叶子节点,就是词法分析阶段生成的 Token(图中带边框的节点)。对这棵 AST 做深度优先的遍历,你就能依次得到原来的 Token。

语义分析(Semantic Analysis)

生成 AST 以后,程序的语法结构就很清晰了
结构的问题解决了,接下来就是语义的问题了
语义分析分为语义理解和语义检查;
语义理解就是让程序理解程序中表达式的含义;
语义检查就是对句子进行检查,排除语法正确但语义有问题的句子

比如说 a+3 这个表达式的含义是,获取变量a的值与3相加再返回。这个表达式我们是很容易理解但编译器不理解啊,所有我们需要给AST每个阶段附加语义规则让编译器去理解,比如:

  • add 节点:把两个子节点的值相加,作为自己的值;
  • 变量节点(在等号右边的话):取出变量的值;
  • 数字字面量节点:返回这个字面量代表的值。

这样的话,如果你深度遍历 AST,并执行每个节点附带的语义规则,就可以得到 a+3 的值。这意味着,我们正确地理解了这个表达式的含义。运用相同的方法,我们也就能够理解一个句子的含义、一个函数的含义,乃至整段源代码的含义。

但是这样还是不够,还要考虑两个问题:

一、变量的作用域问题

  1. int a = 10; //全局变量
  2. int foo(int a){ //参数里有另一个变量a
  3. int b = a + 3; //这里的a指的是哪一个?
  4. return b;
  5. }

这里有两个变量名为a,显然此时我们应该采用就近原则选择参数a而不是全局变量a
为实现这种效果我引入了一个引用消解的概念

引用消解需要在上下文中查找某个标识符的定义与引用的关系,所以语义分析的重要特点,就是做上下文相关的分析

二、变量的数据类型问题

在语义分析阶段,编译器还会识别出数据的类型。比如,在计算“a+3”的时候,我们必须知道 a 和 3 的类型是什么。因为即使同样是加法运算,对于整型和浮点型数据,其计算方法也是不一样的。

语义分析获得的一些信息(引用消解信息、类型信息等),会附加到 AST 上。这样的 AST 叫做带有标注信息的 AST(Annotated AST/Decorated AST),用于更全面地反映源代码的含义。

在这里插入图片描述

好了,前面我所说的,都是如何让编译器更好地理解程序的语义。不过在语义分析阶段,编译器还要做很多语义方面的检查工作。

在自然语言里,我们可以很容易写出一个句子,它在语法上是正确的,但语义上是错误的。比如,“小猫喝水”这句话,它在语法和语义上都是对的;而“水喝小猫”这句话,语法是对的,语义上则是不对的。

计算机程序也会存在很多类似的语义错误的情况。比如说,对于“int b = a+3”的这个语句,语义规则要求,等号右边的表达式必须返回一个整型的数据(或者能够自动转换成整型的数据),否则就跟变量 b 的类型不兼容。如果右边的表达式“a+3”的计算结果是浮点型的,就违背了语义规则,就要报错。

中间代码(Intermediate Representation)

中间代码(IR),是处于源代码和目标代码之间的一种表示形式。

我们倾向于使用 IR 有两个原因。

  1. 很多解释型的语言,可以直接执行 IR,比如 Python 和 Java。这样的话,编译器生成 IR 以后就完成任务了,没有必要生成最终的汇编代码。
  2. 我们生成代码的时候,需要做大量的优化工作。而很多优化工作没有必要基于汇编代码来做,而是可以基于 IR,用统一的算法来完成。

优化(Optimization)

在生成目标代码之前,需要做的优化工作可以有很多,这通常也是编译器在运行时,花费时间最长的一个部分。

那为什么需要做优化工作呢?这里又有两大类的原因。

  1. 源语言和目标语言有差异。
  1. Class Person{
  2. private String name;
  3. public String getName(){
  4. return name;
  5. }
  6. public void setName(String newName){
  7. this.name = newName
  8. }
  9. }

如果你在程序里用“person.getName()”来获取 Person 的 name 字段,会是一个开销很大的操作,因为它涉及函数调用。

怎样简化呢?就是跳过方法的调用。我们直接根据对象的地址计算出 name 属性的地址,然后直接从内存取值就行。这样优化之后,性能会提高好多倍。这种优化方法就叫做内联(inlining),也就是把原来程序中的函数调用去掉,把函数内的逻辑直接嵌入函数调用者的代码中。

总结起来,我们在把源代码翻译成目标代码的过程中,没有必要“直译”,而是可以“意译”。这样我们完成相同的工作,对资源的消耗会更少。

  1. 程序员写的代码不是最优的,而编译器会帮你做纠正。
  1. int bar(){
  2. int a = 10*10; //这里在编译时可以直接计算出100这个值,这叫做“常数折叠”
  3. int b = 20; //这个变量没有用到,可以在代码中删除,这叫做“死代码删除”
  4. if (a>0){ //因为a一定大于0,所以判断条件和else语句都可以去掉
  5. return a+1; //这里可以在编译器就计算出是101
  6. }
  7. else{
  8. return a-1;
  9. }
  10. }
  11. int a = bar(); //这里可以直接换成 a=101

整个对 bar() 函数的调用,可以省略,因为 bar() 的值一定是 101。这些优化工作都可以在编译期间完成。

生成目标代码

编译器最后一个阶段的工作,是生成高效率的目标代码,也就是汇编代码。这个阶段,编译器也有几个重要的工作。

  1. 要选择合适的指令,生成性能最高的代码。
  2. 要优化寄存器的分配,让频繁访问的变量(比如循环变量)放到寄存器里,因为访问寄存器要比访问内存快 100 倍左右。
  3. 在不改变运行结果的情况下,对指令做重新排序,从而充分运用 CPU 内部的多个功能部件的并行计算能力。目标代码生成以后,整个编译过程就完成了。

总结

在这里插入图片描述

在这里插入图片描述

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小丑西瓜9/article/detail/464322
推荐阅读
相关标签
  

闽ICP备14008679号