C编译器剖析_5.2.2 中间代码生成及优化_再论符号symbol与公共子表达式

5.2.2    再论符号symbol与公共子表达式

在介绍算术表达式的翻译前,让我们简单重温一下第2.5节中的“图2.5.4 公共子表达式”及“图2.5.5 valueDef和valueUse”。为阅读方便,我们再次给出这两张图,更详细的说明请参见第2.5节。对于图2.5.4第2行的a+b,我们会由第7行的中间代码来对a+b进行求值,其结果存于临时变量t1中,之后在第3行中再次遇到表达式a+b时,a和b的值并没有发生变化,我们可在第9行直接把t1赋给变量d。由于我们在第4行对a进行赋值,导致t1中保存的值不再有效,所以我们需要重新进行计算第5行的a+b。我们用形如第7行的“t1:a+b;”来表示临时变量t1由a和b相加产生,这与编译原理相关教材中使用“t1
= a+b;”的式子略微有所区别。

图2.5.4  公共子表达式

对公共子表达式进行重用的思路并不难理解,第2.5节的图2.5.5主要是用于说明要实现这样的想法,我们需要定义出相应的数据结构。图2.5.5中的struct  valueDef结构体用来描述一个公共子表达式“t1:a+b”,其中临时变量t1、变量a和变量b各用一个struct  variableSymbol符号对象来刻画。在变量a发生变化时,为了能让公共子表达式a+b失效,我们需要在a中记录“a在哪些公共子表达式中被使用”,图2.5.4中定义的结构体struct
 valueUse就用于此目的。由于变量a可在多个公共子表达式中被使用,因此我们需要一条由若干个struct  valueUse对象构成的链表来记录这些表达式,其链首就存于图2.5.5的结构体对象struct   variableSymbol的uses域中。当a发生变化时,我们要沿着uses域所指向的链表,使链表上的各个公共子表达式失效。当然,变量b中也有类似的结构,当变量b发生变化时,我们也要做类似的处理。

图2.5.5  valueDef和valueUse

图2.5.4第7行使用“t1:a+b”的一个原因是,这隐含着UCC编译器只对t1进行唯一的一次赋值(单赋值)。但对于C语句“b = a > 3? 50:60;”来说,与其对应的中间代码如下所示,我们可以发现临时变量t0会被赋值两次。为了与单赋值的情况有所区别,此处我们用MOV指令“t0 = 50”,其中用的是赋值号’=’,而非冒号’:’。在生成中间代码后,我们在进行优化时,可把以下对t0进行赋值的语句改为对b进行赋值。但由于UCC编译器的优化只在一个基本块内进行,而此处的t0显然出现在多个基本块中,这就需要我们在生成中间代码时做一些特殊处理,以便优化时可对“t0=50;”指令做修改。产生这样困境的原因在于UCC编译器在翻译表达式“a
> 3? 50:60”时,并没有考虑其所处的上下文,而总是将条件表达式的结果先存放到一个临时变量中。

if (a <= 3) goto BB2;

BB1:

t0 = 50; //  优化后改为 b = 50;

goto BB3;

BB2:

t0 = 60;//  优化后改为 b = 60;

BB3:

b = t0; //   优化后可删去

由于UCC编译器在翻译函数调用时,也没有考虑函数调用所处的上下文,总是先把函数调用的返回值存于临时变量中,因此我们就会得到以下两条中间代码。在优化阶段,我们完全可以把这两条中间代码改为“num: f();”。这就需要我们删去“num =t1;”,同时把指令“t1 : f();”改为“num:f();”。由于函数调用是有副作用的,因此函数调用f()不可以作为公共子表达式使用。若num又是一个临时变量,若还存在形如“num
= 50;”这样的MOV指令,则我们还要考虑对“num:f();”和“num=50;”做进一步的优化。在遇到形如“b = a > 3? f():50;”的C语句时,就会出现这样的情况。因此,在为函数调用生成中间代码“t1:f()”时,我们需要做一些特殊的预处理,以便对中间代码进行修改。

t1 : f();

num = t1;

打印出来的中间代码,对临时变量的赋值号是用‘=’还是‘:’,其实只是为了方便中间代码的阅读。UCC编译器中的函数GenerateAssign用于生成形如“t1:a+b;”的中间代码,该中间代码的运算符实际上是ADD,其中的冒号很清楚地告诉我们这不是一条MOV指令,而是ADD指令。而函数GenerateMove则用于生成形如“t0 = 50;”的中间代码,运算符是MOV,其中的‘=’一目了然地告诉我们这是一条MOV指令。函数GenerateFunctionCall用于生成形如“t1
: f();”的中间代码,该中间代码的运算符实际上是CALL。这几个函数的代码如图5.2.6所示,第1至12行是函数GenerateMove的代码,第2行用于生成一条中间代码,第3行把“源操作数src和目的操作数dst”的引用计数加1,第4至5行对中间代码进行初始化,通过第6行的AppendInst函数把MOV指令(形如“t1 = 50;”)添加到当前基本块中。如果目的操作数dst是变量(全局、静态或局部变量),由于当前生成的MOV指令会对dst重新赋值,这就使得以dst作为操作数的公共子表达式失效,第8行的TrackValueChange就会沿着图2.5.5中的uses链表来完成这个工作。如果dst是临时变量,为了能在后续优化时,能对当前MOV指令进行修改,我们在第11行传递给DefineTemp函数的第3个实参(即inst)是MOV指令的首地址,而不是算术运算的操作数。与之形成对比的是用于产生公共子表达式(形如“t1:a+b”)的函数GenerateAssign,如图5.2.6第28至41行所示,我们在第40行传递给DefineTemp函数的实参是“目的操作数dst、运算符的编码opcode、源操作数src1和源操作数src2”。图5.2.6第13至27行的GenerateFunctionCall函数用于生成CALL指令(形如“t1:f();”),第13行的recv对应函数返回值t1,第14行的faddr相当于函数的首地址f,第14行的args向量用于存放多个实参,第16至20行会把这些符号对象的引用计数加1,第21至24行用于初始化CALL指令,并添加到当前基本块中。在后续优化时,可能要修改当前CALL指令,因此在第26行调用DefineTemp函数时,第3个实参仍然是CALL指令的首地址inst,这与MOV指令的情况类似。我们在GenerateMove的第10行调用DefineTemp函数,是为了把“对同一临时变量进行赋值的多条MOV指令”链接到一起,以便后续的优化。而在函数GenerateAssign中第40行调用DefineTemp函数,则确实为了创建一个struct
 valueDef对象来表示公共子表达式。

图5.2.6 GenerateMove()

图5.2.6第42至62行的函数DefineTemp用来创建一个struct  valueDef对象,该对象描述了一个公共子表达式,第44至48行对其进行初始化。如果当前指令是MOV或CALL指令时,通过第51至52行的链表插入操作,我们把对同一个临时变量进行赋值的若干条指令链接到一起。例如,在介绍“b=a > 3? 50:60”的中间代码时,我们遇到了“t0=50;”和“t0=60;”这两条指令,图5.2.6第51至52行会把这两条MOV指令链到一起,链首存放在t0对应符号对象的def域中。之后在执行优化函数PeepHole时,我们遇到中间代码“b
= t0;”时,就可通过t0对应符号对象的def域,找到这些中间指令,并把其中的t0都改为b,从而就可以得到“b=50;”和“b = 60;”这两条优化后的指令,之后还可删去“b = t0;”。当源操作数src1是“局部、静态或全局变量”,我们要在src1变量的uses域所指向的链表上添加一个struct  valueUse对象,用来记录src1会在公共子表达式(t:  src1 op  src2)中被使用,这个工作由图5.2.6第56行的TrackValueUse函数来实现。当然如果存在源操作数src2,我们也做类似处理,图5.2.6第58至60行用于此目的。用于对MOV指令和CALL指令进行优化的主要代码,如图5.2.7所示。

图5.2.7 PeepHole()

图5.2.7第6至24行的的代码用于对CALL指令进行优化,我们想把“形如第11和12行注释里的中间代码”优化为第14行的中间代码,第16行用于把临时变量t1的引用计数减2,第17行实现了把“t1:f();”改为“num:f();”,第18至20行用于删除指令“num = t1;”。当num本身又是临时变量时,我们需要把指令“num:f()”添加到num对应符号对象的def域中,以便后续的进一步优化,这是通过在第22行调用DefineTemp函数来实现。图5.2.7第25至61行用于对MOV指令进行优化,我们希望能把“第28至32行的中间代码”优化为第34至38行的代码,注释中的例子就是我们前面在介绍C语句“b
= a > 3? 50:60;”时遇到的中间代码。当前MOV指令形如第32行的“b=t0;”,其中t0为临时变量,由于在这情况下,对t0进行赋值的指令(第28行的“t0=50;”和第30行的“t0=60;”)都不在当前基本块中,我们可通过第40行的代码,取出我们之前在def域中保存的MOV或CALL指令链表,第40至47行的while循环用于把链表中的形如“t0=50;”的指令改为“b=50;”。我们在第45行要把t0的引用次数减1,在第46行把b的引用数次加1。由于我们要删除第38行的“b=t0;”,就需要在第50和51行把b和t0的引用次数都减1,第58至60行从当前基本块中删除了该指令。如果b本身也是个临时变量,我们就把原先所有对b进行赋值和对t0进行赋值的指令链接到一起,为对b的进一步优化做好准备,第55行的函数AppendVarDefList实现了这两个链表的合并操作(优化前对b进行赋值的指令构成一个链表,而对t0进行赋值的指令又构成另外一个链表,合并之后,新的链表中就可能有CALL指令,也可能有MOV指令,例如“b
= a > 3? f():50;”对应的中间代码)。函数AppendVarDefList的代码并不复杂,我们从略。

公共子表达式“t1:a+b”中的临时变量t1则由C编译器产生,并不是由C程序员给出。而源操作数a和b可能是临时变量,也可能是由C程序员命名的变量。全局变量或静态变量对应“全局静态数据区”中的一块内存单元,而局部变量或者形式参数则对应“栈区”中的一块内存单元。临时变量通常只用于暂存一个计算结果,对应的是“栈区”中的一块内存单元或者直接对应C编译器分配的一个寄存器。这些临时变量名对C程序员是不可见的,C程序员不可能对其进行赋值。

图2.5.5中的struct  valueDef刻画了一个公共子表达式,第2.5节中的图2.5.8中的哈希表valueNumTable用于存放多个公共子表达式。第2.5节“图2.5.9”介绍的TryAddValue函数实现了该哈希表的查找或插入操作。当我们遇到形如a+b这样的表达式时,我们通过调用TryAddValue函数,先在哈希表中找一找,看看在同一基本块内,之前是否已经计算过a+b,若a+b的值仍然有效(即a和b都没有被改动),则没有必要重新计算。但是,由于C语言中可以取a或b的地址,之后再通过地址去访问a或b对应的内存单元,而不必通过变量名a或b来访问,因此,要检测操作数a或b是否被改动过,并不是件容易的事情,这需要进行较复杂的别名分析(Aliase
Analysis)。为简单起见,UCC编译器采取了保守的策略,即一个变量a若被进行过“取地址”运算(即&a),即认为a可能发生变化,公共子表达式a+b不再有效。

在C语言中,对数组元素和结构体成员的访问方式也较为灵活,UCC编译器为了简单起见,也认为这些操作数被进行了“取地址”操作,即不再重用含有这些操作数的表达式。图5.2.8中的例子对此进行了说明,虽然第20行只是对a进行取地址,并没有通过*ptr来改变a的值,但UCC编译器保守地认为a的值会发生变化,因此对第21行的a+b进行重新计算。而含有结构体成员dt.a或者数组元素arr[0]的表达式,也不被当作公共子表达式来处理。在第23行对dt.a+dt.b进行了重新计算,与之对应的中间代码在第48行,其中的dt[0]对应的就是dt.a,而dt[4]对应的就是db.b。在中间代码层次,通过保存在符号表中的类型信息,我们可以知道dt.a在结构体对象dt中偏移为0,而dt.b在结构体对象dt中的偏移为4。这是一种“基地址base
+ 偏移 offset”的寻址模式,符号dt相当于是基地址,而0或4为偏移。在图5.2.8第25行,我们也对C表达式arr[0]+arr[1]重新计算,与之对应的中间代码在第52行。在中间代码层次,我们已经把C程序员编写的第25行的arr[1],改用“基地址base + 偏移offset”的形式来表示,即表示为第52行的arr[4],其中arr相当于基地址,而4相当于偏移。不过,对于第9行的数组int  arr2[3][5]来说,由于C程序员在第26行用arr2[i][2]这样的方式来访问数组元素,其中i为变量,而2为常量,这就意味着对数组元素的寻址要按“基地址+常量偏移+非常量偏移”来进行,其中常量偏移为2*sizeof(int),即8;非常量偏移则为i*sizeof(int)*5,即i*20,第54至57行用于计算出arr2[i][2]的地址并存于临时变量t12中。对图5.2.8第58行的中间代码“*t12
= 30;”而言,若把t12的值存于CPU的寄存器中,我们就可以通过寄存器间接寻址来对arr2[i][2]进行赋值操作。

图5.2.8 取地址与偏移

我们再来看一下图5.2.8第29行的dt.num[3],dt.num在结构体对象dt中的偏移为8,而num[3]在数组dt.num中的偏移为12,两者相加,可得到dt.num[3]在结构体对象dt中的偏移为20。因此,与第29行dt.num[3]对应的是第59行的dt[20],我们仍然是用“基地址+偏移”的模式来访问dt.num[3]。

对图5.2.8第16行的局部变量dt2的初始化而言,在第4.4节对初始化进行语义检查时,我们已介绍过如下所示的struct  initData结构体,第15行的注释表示由3个struct  initData对象构成的链表,其中的(8:3)表示我们要用表达式3,来初始化局部变量dt2从偏移8开始的内存单元,对应的中间代码为图5.2.8第38行的“dt2[8] =
3;”。在中间代码层次,我们还是用 “基地址+偏移”的模式来访问要被初始化的内存单元。不过对于局部变量dt2来说,由于其对应的内存单元在动态分配的栈区中,在汇编代码中,我们就没办法使用变量名dt2,而是使用形如“movl $3,-40(%ebp)”这样的指令,寄存器ebp在运行时会指向动态分配的栈区。图5.2.8第35行的中间代码“dt2:24”表示,要把对象dt2所占24字节栈空间先清0,然后再通过第36至38行的中间代码对相应偏移位置进行初始化操作。

struct initData{

int offset;

AstExpression expr;

InitData next;

};

这里我们初步介绍了对结构体成员或数组元素进行寻址的概念,在下一节中,我们对“偏移”做进一步讨论,分析UCC编译器中与此相关的函数,如Offset()等。

时间: 2024-10-01 03:45:55

C编译器剖析_5.2.2 中间代码生成及优化_再论符号symbol与公共子表达式的相关文章

C编译器剖析_5.2.4 中间代码生成及优化_后缀表达式的翻译

5.2.4 后缀表达式的翻译 在前面的章节中,我们介绍了用于对数组元素和结构体成员进行访问的函数Offset,其接口如下所示,参数addr代表了基地址,参数voff代表可变偏移,而参数coff则代表常量偏移. Symbol Offset(Type ty, Symbol addr,Symbol voff, int coff): 函数Offset的基本想法是产生以下中间代码,我们要先对addr.voff和coff进行相加,得到目标地址(addr+voff+coff),然后再进行"提领Derefere

C编译器剖析_5.4.2 中间代码生成及优化_基本块的合并

5.4.2  基本块的合并 我们在第5.4.1节时给出了由基本块构成的双向链表和控制流图,为阅读方便,我们这里再次给出"图5.1.4 基本块的静态结构和动态结构".在这一小节中,我们试图把双向链表中相邻的基本块进行合并,当然这种合并需要满足一定条件,同时要保持程序的原有语义.在合并后,控制流图中的前驱与后继关系也要进行调整.我们需要改动的数据结构有图5.4.1中的双向链表和控制流图.需要注意的是,虽然基本块BB5和BB6在双向链表中相邻,但控制流却不会由BB5流入BB6,双向链表只是维

C编译器剖析_5.3.1 中间代码生成及优化_If语句和复合语句的翻译

5.3.1   If语句和复合语句的翻译 我们先简单回顾一下对布尔表达式的翻译,我们通过调用TranslateBranch函数来产生跳转指令,从而实现布尔表达式的语义.在使用函数TranslateBranch(expr, bt, bn)时,有这么两个约定: (1) 当expr为真时,跳往bt基本块: (2) 紧随"函数TranslateBranch所生成的跳转指令"之后的基本块为bn. 在表达式的基础上,我们来讨论一下语句的翻译.图5.3.1用于if语句的翻译,第4至8行说明了如何翻译

C编译器剖析_5.3.2 中间代码生成及优化_switch语句的翻译

5.3.2.Switch语句的翻译 在这一小节中,我们来讨论一下switch语句的翻译,switch语句的产生式如下所示. SwitchStatement: switch( expr ) statement 当C程序员编写出如下代码时,UCC编译器会在语义检查阶段进行报错"error:The  break shall  appear  in  a  switch  or loop",从语法上来看,以下"case  3: b = 30;"是case语句,而"

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

5.1  中间代码生成与优化_简介 在语法分析和语义检查阶段,我们始终在与语句Statement.表达式Expression和外部声明ExternalDeclaration这3个概念打交道.通过声明,我们最终建立起了相应的类型结构,并在符号表中保存了相关标识符的类型信息,到了中间代码生成阶段,我们就不再需要处理外部声明ExternalDeclaration了,只需要为语句和表达式生成相应的中间代码即可.文件ucl\tranexpr.c用于为表达式生成中间代码,文件名tranexpr是Transl

C编译器剖析_5.1 中间代码生成及优化_布尔表达式的翻译

5.2  中间代码生成与优化_布尔表达式的翻译 我们仍然按照语法分析和语义检查时的思路,先讨论表达式的翻译,再处理语句.表达式从概念上来说,可分为算术表达式和布尔表达式,在一些编程语言(例如Java)中对这两者是有严格区分的,算术表达式的结果是整数或浮点数,而布尔表达式的结果是逻辑上的真或假.布尔是英国数学家,由于布尔较早进行了关于"与或非"逻辑运算的研究,为了纪念这位先驱,在Java中引入了关键字boolean,而在C++中引入了bool关键字来表达逻辑上的真或假.C语言中并没有专门

C编译器剖析_6.3.1 汇编代码生成_由中间指令产生汇编代码的主要流程

6.3.1  由中间指令产生汇编代码的主要流程 在这一小节,我们可把关注的焦点放在"如何把某条中间代码翻译成汇编代码"上.UCC编译器的中间代码是如下所示的四元式,包括运算符和3个操作数. <运算符opcode,目的操作数DST,源操作数SRC1,源操作数SRC2> 当然有些中间代码只需要用到opcode和DST就可以了,例如,无条件跳转指令"goto  BB2;"就不需要SRC1和SRC2.为了便于汇编代码的生成,UCC编译器在ucl\X86Linux

C编译器剖析_6.3.3 汇编代码生成_为跳转指令产生汇编代码

6.3.3        为跳转指令产生汇编代码 在这一小节中,我们要为"有条件跳转"."无条件跳转"和"间接跳转"产生相应的汇编指令.中间指令的四元式如下所示: <运算符opcode,目的操作数DST,源操作数SRC1,源操作数SRC2> (1) 有条件跳转,例如"if (a <= b) goto BB2;",四元式为: <JLE,BB2,a,b> ////////对应的汇编代码////////

C编译器剖析_6.3.6 汇编代码生成_为“取地址”产生汇编指令

6.3.6  为"取地址"产生汇编指令 在这一小节中,我们来讨论一下以下两条中间指令的翻译: (1)取地址指令<ADDR,DST,SRC1,NULL> 例如 <ADDR,t0,  number, NULL>,表示取number的地址并保存到临时变量t0中 (2)对象清零指令< CLR,DST,SRC1,NULL> 例如<CLR,arr,16,NULL>,表示把arr所占16字节的内存清零 我们先举一个例子来说明,对于图6.3.14第4行局