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

C编译器剖析_6.2 汇编代码生成_寄存器的管理

2015-04-26 22:04 323 查看
在计算机中,CPU的速度比内存的速度快得多,编译器应尽量有效地利用寄存器资源,减少对内存的不必要访问,从而提高由编译器生成的汇编代码的运行速度。在中间代码生成阶段,UCC编译器用临时变量t来存放形如“t: a+b;”的公共子表达式的值;到了汇编代码生成时,UCC编译器会尽可能地把这些公共子表达式的值存放在寄存器,当需要再次重用时,就可以直接由相应的寄存器中得到。不过,CPU中寄存器的资源是很有限的,在32位的x86
芯片上,汇编程序员可用的寄存器有{eax,ebx, ecx,edx, esi,edi,esp,ebp},不过寄存器esp一般用于指向栈顶,而ebp一般用于指向活动记录的底部。真正可供选择的寄存器就只有{eax,ebx,ecx,edx,esi,edi}这6个,当公共子表达式的个数比可用寄存器更多时,我们就要把某些寄存器的值回写(WriteBack)到“临时变量对应的内存单元”中,以便腾出可用寄存器来存放其他值。当然如果待回写的寄存器中的值已经不再需要,或者从内存载入CPU后并没有发生变化,我们就不必浪费时间去做回写操作了。这有点类似操作系统请求分页中的页面置换算法。为了简化寄存器的分配算法,UCC编译器只为临时变量分配寄存器,这意味着我们希望尽可能地重用形如“t:
a+b;”这样的公共子表达式。我们还是用一个简单的例子来说明一下UCC编译器的寄存器分配,如图6.2.1所示,第2至8行为函数f的代码,第9至13行为函数g的代码,在函数f第5行完成对s3的赋值后,我们有意在第6和第7行再次使用公共子表达式(a+b)和(c+d)。第32至46行是函数f对应的汇编代码,而第48至58行是函数g对应的汇编代码。



图6.2.1 寄存器分配的例子
由图6.2.1第15至23行的中间代码,我们可以发现函数f中有3个临时变量,每个都用于存放整数,共占用12字节的栈空间,第34行的“subl $12, %esp”用于在栈中开辟12字节内存来存放这3个临时变量。虽然我们通过第36行的“movl a,%eax”指令,把全局变量a从内存载入寄存器eax中,但在执行第37行的“addl b,%eax”指令后,寄存器eax中保存的就是临时变量“t0:
a+b”的值,之后寄存器eax就一直分配给t0 ,直到t0不再被使用,或者寄存器不够用时。可以发现,在第42行和第45行,我们都重用了保存于寄存器eax中的公共子表达式(a+b),在第44行我们把“(a+b)+(c+d)”的值保存在寄存器edx中。与之形成对比的是在函数g的第57行,我们把“(a+b)+(c+d)”的值存于寄存器eax,其原因是在第57行后,基本块BB1中不再使用第25行的临时变量“t0: a+b”。
还需要注意的是,一条中间代码可能对应若干条的汇编指令,例如图6.2.1第16行的“t0:a+b;”就对应图6.2.1第36和37这两条汇编指令。
如果把寄存器分配的范围扩大到有名的变量(即C程序员命名的全局、静态和局部变量),还可进一步地减少内存的访问次数。例如,当我们把全局变量a读入某个寄存器R1后,之后再次需要a的值时,可直接由寄存器R1得到,不必再去访问内存。但是由于C程序员既可通过变量名来访问“有名的变量”,也可以通过变量的地址addr来间接访问,其访问方式比较灵活。如果通过*addr的形式来访问全局变量a,我们可能会把a的值加载到另一个寄存器R2中,对全局变量a再进行写操作时,就可能出现寄存器R1和R2中的内容不一致的问题。为了避免这样的问题,编译器需要做较复杂的分析。而临时变量只由编译器产生,对C程序员不可见,通常情况下,UCC编译器只对临时变量赋值一次,例如图6.2.1第15至29行的临时变量。不过有个例外,UCC编译器在处理形如“a>0?b:c”的条件表达式时,确实会对临时变量进行多次赋值,稍后我们会对此进行讨论。
为了简单起见,避免复杂的数据流分析,UCC编译器只为临时变量分配寄存器。接下来,我们来看一下用于分配寄存器的函数GetRegInternal,如图6.2.2第2至18行所示,第2行的参数width代表所需要寄存器的宽度,可以是1字节(对应寄存器al、cl和dl),也可以2字节(对应寄存器ax,cx,dx,bx,si和di),还可以是4字节的寄存器(对应eax,ecx,edx,ebx,esi和edi)。第12行调用FindEmptyReg函数来获取还未被分配的寄存器,如果不存在空寄存器,我们就要在第14行通过SelectSpillReg函数选择一个要回写的寄存器,再通过第15行的SpillReg函数将该寄存器中保存的值写回内存,这样该寄存器又可再次被分配。第17行在变量UsedRegs中设置相应的标志位,表示第i个寄存器已经被已经被使用。UCC编译器在为一条中间代码生成汇编指令前,都会先把变量UsedRegs清0,在稍后分析函数EmitBlock时,可以看到这一点。第1行的变量UsedRegs用于记录“已经分配给当前中间代码”的各个寄存器。



图6.2.2 GetRegInternal()
图6.2.2第19至28行的函数FindEmptyReg用于查找未被分配的寄存器,第22行的条件“X86Regs[i] !=NULL”会排除esp和ebp这两个栈寄存器,第23行的条件表示寄存器中没有保存临时变量的值(空寄存器EmptyRegister),第24行的条件表示该寄存器还没有分配给当前中间指令。若找不到空寄存器,则在第27行返回NO_REG。当所有寄存器都被分配完了,我们需要通过第29至48行的函数SelectSpillReg选择一个要淘汰的寄存器。这很像请求分页系统中的页面置换算法,我们需要按FIFO或LRU等算法来淘汰一些页面。UCC编译器会根据“寄存器对应临时变量的引用次数总和”来做选择,回写引用次数最少的寄存器,第32至45行对此进行处理。第48至58行的函数SpillReg会把保存在寄存器中的各临时变量的值写回内存,这个动作常被称为“寄存器溢出”。图6.2.2第52行的“p->needwb”不为0时,表示临时变量p在寄存器和内存中的值已经不一致,而“p->ref >
0”表示临时变量p还需要再次被使用,当这两个条件都符合时,我们才会调用53行的StoreVar函数来产生写内存的指令。当然,在目前版本的UCC编译器中,一个寄存器通常只保存一个临时变量的值,因此第38行和第50行这两个while语句的循环体只执行一次。换言之,第37行的链表X86Regs[i]->link和第49行的链表reg->link上的元素个数都不会超过1个。第51行设置p->reg为NULL,表示临时变量p的值不再保存在寄存器中。
为了加快浮点数的运算,Intel还提供了一个浮点协处理器X87,X87提供了由多个浮点寄存器构成的栈,但为了简单起见,UCC编译器实际上只使用位于栈顶的浮点数寄存器,来保存某个浮点数临时变量。UCC编译器中的指针变量X87Top用于指向该临时变量,当X87Top不为NULL时,表示相应临时变量的值保存在协处理器X87的栈顶。
static Symbol X87Top;
在此基础上,我们来看一下“为基本块产生汇编代码”的函数EmitBlock,如图6.2.3所示第1至24行所示,第3行的while循环会遍历基本块中的所有中间代码,第10行调用的EmitIRInst函数实现了为中间代码inst产生汇编指令的操作。由图6.2.3第25至29行可发现,这是通过查表来实现相应函数的调用,第27行的表格Emitter中存放了形如第33行的EmitJump的函数名。第33至38行的EmitJump函数用于为无条件跳转指令产生汇编代码。



图6.2.3 EmitBBlock()
虽然我们在中间代码生成阶段,只对同一基本块内的公共子表达式进行重用,但在遇到条件表达式“(a>0?b:c)”时,确实存在“一个基本块内的临时变量,可能会在其他基本块中被使用”的情况,如下所示,临时变量t0会在多个基本块中被赋值,当我们通过“gotoBB5”离开基本块BB3时,我们需要回写临时变量t0的值。
d = (a>0?b:c)+1;
///////////////对应的中间代码/////////////////
if (a <= 0) goto BB4;
BB3:
t0 = b; //对临时变量t0的赋值
goto BB5;
BB4:
t0 = c; //对临时变量t0的赋值
BB5:
t1 : t0 + 1;
d= t1;
因此,当控制流要通过跳转语句离开基本块,或者遇到函数调用时,我们要对寄存器进行回写操作,图6.2.3第8行调用的SaveX87Top用于回写X87的栈顶寄存器,而对X86寄存器的回写操作,我们将推迟到EmitJump、EmitBranch、EmitIndirectJump和EmitCall等函数中执行,分别对应“无条件跳转”,“有条件跳转”,“通过跳转表进行跳转”和“函数调用”。按C标准的规定,在函数调用时,主调函数只要回写eax、ecx和edx这3个寄存器即可。而在遇到跳转语句时,我们通过调用ClearRegs函数来回写“eax、ebx、ecx、edx、esp和ebp”这6个寄存器,如图6.2.3第37行所示。
函数SaveX87Top的代码在图6.2.3第48至57行,第53行调用的PutASMCode函数会产生浮点数的回写指令,我们会在后续章节对PutASMCode函数进行分析,第56行置X87Top为NULL,表示已不存在需要回写的临时变量。
我们还发现,在控制流离开上述基本块BB4时,我们也要把临时变量t0的值写回内存,这可通过图6.2.3第22行调用的ClearRegs和第23行调用的SaveX87Top函数来实现。当我们在图6.2.3第10行为一条中间代码产生汇编指令后,可在第14至18行把各操作数的引用计数减1,这会对前文所述选择溢出寄存器的SelectSpillReg函数产生影响。
在后续章节中,我们会对EmitBranch、EmitIndirectJump和EmitCall等为中间代码生成汇编指令的函数进行讨论。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: