编译器,优化,及目标代码生成.
本文介绍从源文件开始到目标代码生成的过程.
- 首先,是我们每天都要接触的源文件.源文件是由纯ASCII或者其他字符集组成的文本,由程序员使用文本编辑器创建.它有以下的几种形式
- 纯文本.好处是易于维护.并且可以使用处理文本文件的程序来处理源文件.
- 这个就是我们最常见的源代码形式了.甚至可以使用notepad来处理源文件!
- 记号化的源文件.使用专门的单字节"记号"值来表示源文件中的保留字等语句元素.
- 好处1:尺寸小,由于使用单字节的符号来"压缩"多字符的保留字,所以比纯文本源文件小.
- 好处2:由于识别单字节比识别多字符高效,所以使得解释器效率更高.
- 好处3:基于记号化的源文件,也很容易的构建出原先(或者类似的)文本源文件.
- 缺陷1:会丢弃空白区域.
- 缺陷2:引入了专有的格式.不方便使用普通的文本文件处理程序来进行处理.
- 专门的源文件格式.
- 使用图形元素来表示程序要完成的指令.如Delphi等.
- 纯文本.好处是易于维护.并且可以使用处理文本文件的程序来处理源文件.
- 源文件编写完成后,要在计算机上运行,需要使用计算机处理程序来进行处理.主要有以下几种的处理程序.
- 纯解释器.
- 工作方式:直接工作于文本源文件.持续扫描源文件,将其作为字符串进行处理.
- 问题:没有效率.在识别"词素(lexeme)"时的耗时,甚至会大于程序实际的执行时间.
- 适用场景:1)期望语言处理程序非常紧凑时.2)脚本语言和超高级语言(在程序执行期间将源代码当做字符串操作).
- 解释器
- 工作方式:运行时,执行源文件的替身.对符号化的源文件(对文本源文件需要进行符号化的转换)进行操作,省去了执行时的词素分析.
- 问题:无法将字符串当做程序语句进行执行.
- 编译器
- 工作方式:在运行之前,先将源文件转换为可执行的机器码.
- 特性:生成的机器指令可以由CPU直接执行.所以运行时,所有的资源都可以用来执行机器码,而不必浪费时间解析源文件.
- 问题:源文件到机器码的转换是单向的,很难进行逆向的操作.
- 增量编译器
- 工作方式:编译器和解释器的交集.将源代码转化为某种中间形式.中间形式与原始文件的联系不紧密,而通常是"虚拟(假想)机器"的机器码,而不是可以执行在物理CPU上执行的机器码.然后对虚拟机编写解释器,然后它来实际执行代码.
- 优势:虚拟机具有可移植性.而真正的机器码只能在特定的CPU上执行.这样是JAVA宣称的"一次编译,到处运行".
- 可以使用"即时编译"来提升性能.
- 在运行期间,大部分的事件都花在获得并解析虚拟机代码的操作上了.程序执行期间,解释过程会反复进行.
- 在首次遇到虚拟指令时,就将虚拟代码转换为实际的机器码,然后在后续过程需要同一语句时省去解释过程.
- 这样就是很多.NET程序首次启动很慢,而之后速度会变得很快的原因了.
- 纯解释器.
- 语言处理程序在转化源文件到目标代码时,需要经过几个阶段.
- 词法分析
- 扫描程序负责读取从源文件中找到的字符和字符串数据,并将这些数据分类为表示源文件词素项的记号.词素项就是源文件中的字符序列(程序的原子级组件).对每个词素创建一个小的数据包,即”记号”并将此数据包发往语法分析程序.
- 此过程可选,语法分析可直接工作于源文件,但是这样的话,语法分析程序在处理源文件时就得多次引用某记号.通过预处理源文件,将其分类为一系列的记号后,编译过程就更高效.
-
将字符串词素转化成较小的记号包,扫描程序就能让语法分析程序将记号按整数值对待,而无需依照字符串进行操作.CPU处理小值整数比字符串高效,而且语法分析要多次引用记号数据,这样省去很多时间.
-
在分析阶段多次扫描每个记号的语言系统只有纯解释器.所以其很慢.
- 语法分析
- 编译器的一部分.负责检查源程序的语法语义是否正确,且将记号流(即源代码)重组为更复杂的数据结构,使之表示程序的意思即语义.
- 扫描程序和语法分析程序一般以线行方式从头到尾加工源文件,编译器通常只读源文件一次,随后通过构建表示源代码的数据结构(AST:抽象语法树)来随即访问引用源文件体.
- 使得方便地引用程序的不同部分.减轻代码生成和优化阶段的负担.
- 中间代码生成.
- 不直接转化为本机机器码的原因.
- 1)编译器的优化阶段可以进行某些类型的优化(对中间代码形式操作较容易).
- 2)许多编译器是跨平台的,能生成工作于不同CPU架构的机器码.然后将所有不依赖于特定CPU的动作放到中间代码生成阶段,只需生成这些代码一次,而跟特定CPU相关的放到各个执行PC上来进行.
- 不直接转化为本机机器码的原因.
- 优化阶段
- 通常是消除AST中不必要的项目.
- 问题:效率的定义是程序对某些资源的最小占用.主要的资源是内存(体积)和CPU周期(速度).问题在于:朝着某个目标优化,可能与朝着其他目标的优化措施发生冲突.所以优化是个折中过程,需要牺牲某些次要目标来换取某个合理的结果.
- 优化对编译时间的影响:编译器还得在合理的时间内产生可执行的结构,这是个”NP-完全问题(NP-complete problem):正确解显然是存在的,但必须经过海量的计算.计算量之大,使得求出正确解没有可行性.所以我们只能找出可行的近似解”.解决NP-问题所需的时间与输入量呈指数关系.编译器使用启发式和案例性算法来确定生成应采取的转换.
- 基本块,可归约代码和优化.
- 优化时会随着贯穿程序的控制流跟踪变量值,该过程称为”数据流分析”.以确定变量:何处尚未初始化,何时包含某值,何时不再被使用,何时对变量值一无所知.优化是固有的缓慢过程
- 在时间上的让步:在进行下步前,对一段代码找寻较多可能的优化办法.因此编程风格很容易搞糊涂编译器,使得无法产生最优化.
- 将源代码划分为”基本块”的序列.基本块就是顺序执行的机器指令序列,除了块的开始和结束位置外没有任何分支.确定基本块的起止:只要有地方时条件分支/跳转,无条件跳转,或调用指令就表明该基本块到头了.基本块包含将控制发往别处的指令,该指令后是另一基本块的开始处.
- 基本块使得跟踪基本块内对变量及其他数据的操作变得方便.
- 当两个基本块的路径汇集到同一代码流时,某变量可能的值的数目就会随着if语句数量呈指数增长.
- 即使来自于基本块的路径汇集到一起,程序也会经常对变量赋新值,因此编译器不必跟踪旧信息.编译器假设程序不会在每个路径内都对变量赋予不同值,其内部数据结构也据此构建.
- 合理的程序会产生”可归约的流程图”:程序控制流的图形化表示.图内,各基本块的结束处与其传送控制的基本块开始处用箭头连接.
- 不用Goto的程序是可归约的.可归约的程序内的基本块可缩写成提纲方式,后者的块继承了基本块内在的特性.提纲方式使得优化程序只需应对数目较少的基本块,而不是大量的语句.
- 编译器常见的优化措施.
- 常量折叠:在编译时期就计算出常量或子表达式的值,而不是运行时才发送代码去计算结果.
- 常量传播:如果能确定在前面的代码中某变量被赋值以常量,那么将要访问该变量的地方替换成常量值.
- 死代码消除:删除那些与特定源代码语句(其结果从未被使用过,或者条件块从不为True)关联的目标码.
- 公共子表达式消除:如果子表达式中变量值并未改变,就将其缓存,而无需在下次出现时重复计算该表达式的值.
- 强度消弱:采用与源代码不同的运算符(开销小)来直接计算出结果.
- 归纳:许多表达式中某个变量值完全依赖于其他某个变量,这时省去对其新值的计算,或者在循环内将变量值计算与表达式计算合并.
- 循环不变量:不会随着每轮循环改变的表达式,只要在循环外一次计算出其结果,然后”代码移动”将其移出循环体.
- 控制编译器的优化.
- 默认时,编译器不采取任何优化措施,必须明确地告诉编译器执行某些优化.理由:
- 1)优化是漫长的过程;
- 2)优化后许多调试器不能很好地工作;
- 3)编译器的大部分缺陷都在优化程序中.”优化”对不同的人有不同的意义,所以有各种方面的优化.
- 默认时,编译器不采取任何优化措施,必须明确地告诉编译器执行某些优化.理由:
- 编译器的输出.也就是语言处理程序最终将源文件转换的结果.大部分的输出都不是特定CPU能够执行的.
- 1)输出高级语言代码(可读,易验证,跨平台,可利用其他高级语言编译器的优化程序;但耗费更多的处理时间,高级语言很难高效地映射到底层机器码).
- 2)输出汇编语言代码(允许嵌入”内联的汇编语言”语句来满足时间严格要求的场景);
- 3)输出目标文件(需要链接器再加工);
- 4)输出可执行文件.