设为首页 加入收藏

TOP

C编译器剖析-汇编代码生成寄存器的管理(一)
2015-11-19 23:06:52 】 浏览:434
Tags:编译器 剖析 汇编 代码 生成 寄存器 管理

在计算机中,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>0b: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

首页 上一页 1 2 下一页 尾页 1/2/2
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇循环队列(C语言版) 下一篇浅谈C的应用与常见error

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目