您的位置:首页 > 编程语言

C编译器剖析_5.1 中间代码生成及优化_简介

2015-04-08 19:41 931 查看
5.1 中间代码生成与优化_简介
在语法分析和语义检查阶段,我们始终在与语句Statement、表达式Expression和外部声明ExternalDeclaration这3个概念打交道。通过声明,我们最终建立起了相应的类型结构,并在符号表中保存了相关标识符的类型信息,到了中间代码生成阶段,我们就不再需要处理外部声明ExternalDeclaration了,只需要为语句和表达式生成相应的中间代码即可。文件ucl\tranexpr.c用于为表达式生成中间代码,文件名tranexpr是Translate
Expression的缩写,而ucl\transtmt.c则用于为语句生成中间代码。我们在“第1.4节 UCC编译器预览”时给出过以下图5.1.1中的例子,为阅读方便,我们再次重温一下其中的概念。图5.1.1第1至9行的递归函数用于计算n阶乘,第12至15行的while循环用于打印出1!至10!的值,第19至45是“UCC编译器所生成的中间代码”的字符串形式,UCC编译器内部会用三地址码的结构来表示中间代码。“龙书”把语法树和三地址码都视为中间代码,这里我们在讨论UCC编译器时,如果不作特别声明,中间代码指的是三地址码。三地址码包含两个源操作数、一个目的操作数及一个运算符。例如对于t1
= a+b而言,加号”+”是运算符,a和b是两个源操作数,而t1是目的操作数(即用于存放运算结果),这三个操作数对应三个“地址”,所以称这样的中间代码为三地址码。



图5.1.1 基本块
图5.11第20行的中间代码表示有条件的跳转,第23行的goto表示无条件的跳转,在在汇编语言中,都有与之对应的汇编指令。低级语言中的“有条件或无条件的跳转”会引起控制流的转移,由此我们可以实现高级语言C中的分支语句if和循环语句while等控制结构。当然,函数返回也是一种控制流的变化,在UCC编译器生成的中间代码中,第22行的return 1只是把返回值设为1,真正的返回动作由第30行的ret指令来完成。这样处理的好处在于,即便在C语言中出现了多条return语句(如第5行和第7行的return语句),但在中间代码层次,我们只在第22行设置返回值为1,第29行设置返回值为t2,真正的返回动作只需要在同一个地方处理(即第30行的ret指令),这也意味着我们从函数的入口处开始执行,离开函数时也只有一个出口。执行“函数调用”时,我们要依次把实参和返回地址入栈,之后无条件跳转到函数起始地址,因此函数调用也可看成是一种无条件跳转。生成中间代码后,我们还需要进行代码优化,此时我们需要考虑控制流的变化。我们期望把相邻的若干条中间代码当作一个整体来处理,例如第26至29行的这4条中间代码,控制流只能从这个整体的第一条指令进入(此处即第26行对应的中间代码),离开时从基本块中的最后一条指令离开(此处即第29行的中间代码),我们称这样的“整体”为一个基本块BasicBlock,第25行的“BB3”表示第3个基本块。由此,整个C程序就可由若干个基本块构成。
对于图5.1.1第19至45行的中间代码而言,从静态来看,这些基本块是从上至下依次排列的,我们用一个链表就可以存放这些基本块。但从条件跳转和无条件跳转的动态语义来看,例如,我们若把第41行的基本块BB7看成一个结点,则执行完第42行的条件跳转语句后,接下来我们可能执行的是第44行的中间代码,也可能是第37行的中间代码。这就相当于存在一条由第41行的基本块BB7指向第43行的基本块BB8的有向边,同时从BB7到BB6也存在一条有向边。由此,基本块成了点,而“控制流的转移”就成了有向边,我们可把整个程序的动态执行的路线看成数据结构中的“图”,这个由基本块构成的图被称为控制流图Control
Flow Graph,简写为CFG。后续的分析和优化都是基于“控制流图”这样的数据结构进行。需要说明的是,UCC编译器的中间代码优化工作只在基本块中进行,并没有进行函数间(过程间)的代码优化,因此UCC编译器在划分基本块时,并没有把函数调用当作基本块的最后一条指令,例如在图5.1.1第36行的基本块BB6中,第39行的函数调用printf并不是基本块的最后一条指令。在中间代码优化阶段,ucl\simp.c中的PeepHole()函数会对CALL指令进行优化处理,PeepHole译为“窥孔优化”,“窥孔”的含义是我们通过一个小孔或小窗口来看世界,一次只看到世界的一小部分(局部)。在UCC编译器中,这个孔的大小一般就限定在一个“基本块”内。例如,对于以下两条中间代码来说:
t1 = f();
num = t1;
由于UCC编译器没有把函数调用f()置于基本块的最末尾,因此这2条中间代码就可以处于同一个基本块中,在PeepHole()函数对基本块进行“窥孔优化”时,就会发现临时变量t1是多余的,这2条中间代码可优化为如下所示的中间代码:
num = f();
为了方便进行这样的优化,UCC编译器在划分基本块时,并没有把函数调用当作控制流的转移。但我们知道,在真正执行机器指令时,函数调用确实会导致控制流的转移。UCC编译器在后续阶段产生x86汇编指令时,还会在ucl\x86.c的EmitBlock函数中对CALL指令进行特殊判断,从而保存函数调用前用到的一些寄存器。
简单来说,为了存放如图5.1.1第19至45行的中间代码,我们需要由若干个基本块构成的链表结构;为了描述控制流在基本块间的动态转移,我们需要“控制流图”的数据结构。图5.1.2第2至8行的结构体irinst用于描述一条“三地址码”中间代码,第7行的opds[3]用于存放3个操作数,每个操作数由一个符号对象struct symbol来表示,我们在“第2.5节 UCC编译器的符号表管理”中简介过符号symbol的相关概念。第6行的opcode用于存放运算符。由于一个基本块可以包含若干条的中间代码,我们可用第3行的prev和第4行的next来构成双向链表结构,第5行的ty用于记录运算结果的类型信息。



图5.1.2 相关数据结构
图5.1.2第15至32行的结构体用于刻画一个基本块,第16行的prev和第17行的next用于构造由若干个基本块形成的双向链表。双向链表描述了如图5.1.1所示的基本块的“静态结构”;而其动态结构“控制流图CFG”中,一个结点可以有多个前驱,也可以有多个后继,第20行的succs用于记录当前基本块的所有前驱后继结点,而第22行的preds用于记录其所有的后继前驱结点。第25行的ninst用于记录当前基本块中有多少条中间代码,第27行的nsucc用于记录后继结点的个数,而29行的npred则用于记录前驱结点的个数。第18行的sym用于存放基本块的名称,例如”BB3”等。第23行的insth对应一条占位用的中间代码,仅仅用于充当双向链表的头结点,并不对应任何实际的代码。
由于一个基本块Bx可以有多个前驱{B1,B2,B3, …, Bn},每个前驱到基本块Bx都存在一条有向边。同理,该基本块Bx也可以有多个后继,从Bx到各个后继结点也存在相应的有向边。图5.1.2第10至13行的结构体struct cfgedge用于描述一条有向边,第11行的bb域用于存放一条有向边的前驱(或者后继);第12行的next域用于构成若干个前驱(或者后继)形成的单向链表。第49行的函数DrawCFGEdge(head,tail)用于构造一条从基本块head指向基本块tail的有向边,这意味着tail是head的后继结点,我们通过第50行的AddSuccessor函数把tail加入到基本块head的succs域所指向的后继链表中;同时,head是tail结点的前驱,我们需要把head加入到基本块tail的preds域所指向的前驱链表中,这个工作由第51行的函数AddPredecessor来实现。

head --> tail

图5.1.2第54行的函数AppendInst用于往当前基本块中添加一条中间代码,第55至59行的4条语句用于实现双向链表的插入操作。
接下来,我们来初步了解一下中间代码生成的总体执行过程,如图5.1.3所示,第55至64行的Translate函数实现了由抽象语法树AST到三地址码的翻译,第57行的while循环依次对当前C文件中的各个函数进行翻译,实际的工作由第60行的TranslateFunction来完成。图5.1.3第38至54行给出了函数TranslateFunction的主要代码,按照我们前面对return语句的处理,不论函数内部的控制流有多复杂,整个函数定义都只有一个入口和一个出口。第43行第44行分别调用CreateBBlock()来创建这两个基本块,第20至26行给出了CreateBBlock()函数的代码,第23行用于设置头结点为空指令NOP(No
Operation的缩写,即“空指令,无任何实际运算”)。第46行调用TranslateStatement()实现了对函数体的翻译,函数体实际上是一个复合语句CompoundStatement。第1至16行列出了一个函数指针表,第2至15行的各函数完成了各语句的翻译工作,第17至19行的TranslateStatement函数只是查询这个函数指针表而已。我们会在后续章节对第2至15行中的各个函数进行分析。第48行的Optimize()函数用于对生成的中间代码进行优化,第50行的while循环用于给各个基本块命名,形如”BB1”和”BB2”等。图5.1.3第38至54行就是中间代码生成及优化的主要流程。



图5.1.3 Translate()
需要注意的是图5.1.3第43行调用的CreateBBlock()函数返回后,新创建的基本块对象并未加入到双向链表中。只有调用了第27行的StartBBlock(BBlock bb)函数后,我们才会把函数参数bb所指向的基本块对象加入到双向链表中,第29至30行的代码实际了这个插入操作。图5.1.3第31行的if语句检查一下“当前基本块的最后一条指令是否会使控制流转移到参数bb所指向的基本块”,如果可能出现这样的转移,我们就会在第34行调用DrawCFGEdge函数,构造一条由当前基本块指向参数bb基本块的有向边,之后在第36行设置使参数bb成为新的当前基本块。基本块末尾的无条件跳转指令JMP,会导致控制流转移到不相邻的基本块上,例如图5.1.1基本块BB1第23行的goto
BB4指令,就会跳往与BB1不相邻的基本块BB4,对于这种情况,我们会在生成无条件跳转指令时进行有向边的构造,相应的操作可参见ucl\gen.c中的函数GenerateJump。类似的,如果基本块的最末尾一条指令是如下所示的间接跳转,我们也不调用图5.1.3第34行的DrawCFGEdge函数,而是由ucl\gen.c中的函数GenerateIndirectJump()根据实际的跳转目标来构造有向边。这两个函数并不复杂,我们就从略。在进行switch语句的翻译时,我们会用到间接跳转。

//根据t0的值来确定跳转的目标

goto (BB1,BB2,BB3,)[t0];

而如果当前基本块的最末一条指令是“有条件跳转指令”,则控制流还是可能流向相邻的下一个基本块的,此时我们需要调用图5.1.3第34行DrawCFGEdge函数来构造有向边,例如图5.1.1的第42行。当然,如果最末一条指令不是跳转指令,那控制流一定是流向相邻的下一个基本块,此时我们也需要调用图5.1.3第34行的DrawCFGEdge函数,例如图5.1.1的第40行。对于图5.1.1中的main()函数而言,下图5.1.4是其基本块的静态结构和动态结构,静态结构采用的是双向链表,而动态结构采用的是控制流图。



图5.1.4 基本块的静态结构和动态结构
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: