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语句,而“break;”并不是case语句的一部分。按switch语句的产生式,以下“switch(a) case 3:
b = 30;”构成一条完整的switch语句,而“break;”则不在switch语句内。
switch(a)
case 3: b =30; break;
虽然按switch语句的产生式,其中的statement部分并不一定是复合语句,但通常C程序员在编写代码时,会将此部分写为一个复合语句,例如:
switch(a){
case 3: b = 30; break;
}
此时,复合语句中的“break;”就包含在swich语句中,这就可以符合C语言的语义要求,即“break语句要被包含在switch语句或者循环语句”中。到了中间代码生成阶段,我们面对的已经是“没有语法或语义错误”的语法树。我们还是举一个例子来说明switch语句的翻译,如图5.3.3所示,第4至17行为一个switch语句,与之对应的中间代码在第23至40行。我们注意到,第28行是一条“间接跳转”指令,我们根据(a-1)的值来索引跳转表“[BB4,BB8,BB6,BB12,BB10,]”,从而得到相应的跳转目标,例如当a为1时,我们得到的跳转目标为BB4,当a的值为4时,由于4并不落在{1,3,2,5}中,对应的跳转目标BB12用于跳出switch语句。通过跳转表的技术来翻译switch语句,可以使得生成的汇编代码在运行时不需要进行过多的比较,从而加快代码的执行速度,不过,跳转表本身需要占用内存空间,这是一种“以空间换时间”的方法。第44至53行是对应的部分汇编代码,第44至49行的跳转表switchTable1在数据区中,其中存放的是跳转目标的首地址(代码区中的地址),而第50至53行则根据(a-1)的值来查表,再进行跳转。
图5.3.3 switch语句的例子
在图5.3.3的例子中,各case语句中的常量为{1,3,2,5},这些整数在数值上相差不大,如果我们遇到的是{1,2,20000,50},相应的跳转表就得有20000个表项,其中大部分的表项存放的是形如“BB12”这样的用于跳出switch语句的地址,这需要耗费相当大的内存,此时我们不再构造一张跳转表,而是想把{1,2,20000,50}分成几段,每一段内的各个数值彼此接近,例如我们可把{1,2,20000,50}分成3段{{1,2},{50},{20000}}。我们制定一个标准,来衡量各数值彼此接近的程度,UCC编译器引入了一个“Case语句密度”的概念,
Case语句密度 =Case语句数量/ 区间大小
例如,对于{1,2,20000,50}来说,其密度为(4/(20000-1)),这有点像人口密度的概念,反映了人口的密集程度,而{1,2}的密度为2/(2-1)。为了方便代码生成,UCC编译器在语法分析时,会把在同一switch语句的各case语句,按出现的先后顺序进行排列。而为了统计“Case语句密度”,在语义检查时,会按各case语句的常量表达式数值,从小到大进行排列。因此,每条case语句实际上会出两个链表中,UCC编译器用结构体astCaseStatement来描述一条case语句,其中的next和nextCase域用于构造这样的两个链表。
struct astCaseStatement{
//按case语句在源代码中出现的先后顺序排列
struct astNode *next;
//按case语句的表达式数值从小到大排列
structastCaseStatement *nextCase;…..
}
当面对从小到大排列的{1,2,50,20000}时,UCC编译器要求每段的密度要大于
1/2,根据这个经验值,我们可以按以下步骤进行分段,
(1) 第1个case语句就是一段,即{1}
(2) 把第2个case语句加入{1},则有{1,2},其密度为2
(3) 若把第3个case语句加入{1,2},则有{1,2,50},其密度为3/49,小于1/2,因此我新创建一段来存放50,即有了{{1,2},{50}}
(4) 若把第4个case语句加入{50},则有{50,20000},其密度也小于1/2,因此我们再新创建一段来存放20000,即有了{{1,2},{50},{20000}}
若有一个整数val,我们要判断val是落在哪一段中,为了减少比较的次数,我们可以
采用二分查找的方法,即先看看val是否落在中间的那一段{50},如果比中间段更小,就再看看是否能在左侧的段{1,2}中;若更大,则看看是否落在右侧的段{20000}中。按这样思路,我们可以产生如图5.3.4第23至38行的比较和跳转操作,而第39至50行的中间代码仍然保持C源程序中case语句的先后顺序。我们还注意到,当某段中的case语句多于1条时,UCC编译器会通过跳转表来进行跳转,如第34行所示。如果某段中只有一条case语句且其左侧或右侧的段还没有被比较过,例如第23行的{50},我们可产生形如第23至27行的代码,即要处理“小于”、“大于”和“落在段中”这3种情况;而如果该段的左侧和右侧的其他段都已被处理过,例如第36行的{20000},我们可产生形如第36至38行的代码即可,此时只要处理“等于”和“不等于”这2种情况。
图5.3.4 Case语句密度与二分查找
我们再举一个例子来说明分段中可能出现的情况,例如当UCC编译器面对从小到大排列的各case语句{0, 1, 4, 9, 10, 11}时,我们可以按以下步骤进行分段:
(1) 加入case 0,得到{0};
(2) 加入case 1,得到{0,1},密度为2;
(3) 加入case 4,得到{0,1,4},密度为3/4;
(4) case 9单独构成一段,得到{{0,1,4},{9}};
(5) 加入case 10,得到{{0,1,4},{9,10}};
(6) 加入case 11,先得到{{0,1,4},{9,10,11}},由于6/11仍大于1/2,因此我们把这两段合并为 {0,1,4,9,10,11}。
为了描述“段”的概念,UCC编译器引入了switchBucket结构体,Bucket
是“桶”的意思,我们用“Bucket”来存放处于同一段中的各条case语句,由于存在多个分段,就需要多个桶,我们可用链表结构来管理这些桶,而每个桶内又有一条由case语句构成的链表,switchBucket结构体如下所示:
typedef struct switchBucket{//用于描述形如{0,1,4}这样的段
int ncase; //桶内case语句的条数,例如3
int minVal; //桶内case语句表达式的最小值,例如0
int maxVal; //桶内case语句表达式的最大值,例如4
AstCaseStatement cases; //桶内case语句链表的链首
AstCaseStatement*tail; //指向链尾,便于插入操作
structswitchBucket *prev;//用于组成由各个“桶”对象构成的链
} *SwitchBucket;
为了方便对各SwitchBucket进行二分查找,UCC编译器除了把各switchBucket对象通过上述prev域构成一条链表外,还会用一个数组来存放各switchBucket的首地址。例如,对于{{1,2},{50},{20000}}来说,我们可用以下伪代码来表示相关结构:
SwitchBucket ptr1 = {1,2};
SwitchBucket ptr2 = {50};SwitchBucket ptr3 = {20000}
其链表结构为:
{1,2} ---> {50} --->{20000}
其数组结构为:
SwitchBucket bucketArray[] = {ptr1,ptr2,ptr3};
有了这样的基础后,我们就可以来看一下switch语句的翻译,如图5.3.5所示,第7行用于翻译switch语句中的表达式,第11至31行把各case语句加入到相应的桶中。由于每个case语句都是控制流的跳转目标,因此每个case语句都对应一个基本块,第14行调用CreateBlock()函数创建了这些基本块。第16行的if条件用于判断“当前桶中的case语句密度是否大于1/2”,第21行通过调用MergeSwitchBucket函数进行相邻桶的合并,由此可把前文的{0,1,4}和{9,10,11}这两个桶的合并为{0,1,4,9,10,11}。当Case语句密度小于或等于1/2时,我们通过第23至28行创建一个新桶。第32至39行用于创建桶指针数组,便于进行二分查找。当Switch语句的表达式的值不与任何case匹配时,控制流要么进入default语句(在C程序员提供default语句时),要么跳出switch语句(在C程序员没有编写default语句时),第40至46行会对此进行处理。图5.3.5第48行调用TranslateSwitchBuckets函数,用于产生形如“图5.3.4第23至38行的用于比较和跳转”的中间代码。如果switch语句确实包含case或者default语句,第54行就递归地调用TranslateStatement函数,来翻译候选式“switch(expr)statement”中的statement。
图5.3.5 TranslateSwitchStatement()
接下来,我们来分析一下用于产生比较和跳转语句的TranslateSwitchBuckets函数,其函数接口如下所示:
static void TranslateSwitchBuckets(//对bucketArray[left]至bucketArray[right]这几个桶进行处理
SwitchBucket *bucketArray, int left, int right,
//符号choice代表了switch(expr)statement中表达式的值
Symbol choice,
//指向当前要处理的SwichBucket桶或者为NULL
BBlock currBB,
//参数defBB要么是default语句对应的基本块(存在default语句时)
//要么是switch语句之后的基本块nextBB(当default语句不存在时)
BBlock defBB
);
例如,在图5.3.5第48行我们按以下方式来调用TranslateSwitchBuckets函数,其中的swtchStmt->nbucket代表switch语句中case的个数,此处待翻译的各桶的下标从0至(swtchStmt->nbucket –1)。
TranslateSwitchBuckets(bucketArray, 0, swtchStmt->nbucket- 1,
sym, NULL,swtchStmt->defBB);
仍以图5.3.4为例,对于{{1,2},{50},{20000}}而言,按二分查找的算法,我们最先处理的是位于中间的桶{50},我们会为之产生以下用于比较和跳转的代码:
if (a < 50) goto BB4; //再去与桶{1,2}比较
BB1:
if (a > 50) goto BB8; //再去与桶{20000}比较
BB2:
goto BB17; //跳往case 50
图5.3.6给出了TranslateSwitchBuckets函数的代码,第15至18行用于构造长度为len的跳转表,第19至26行用于对跳转表进行初始化,从而得到形如“(BB11,BB13,)”的表格,当桶中只有一个case语句,且该桶左侧或右侧的其他桶都已被处理完,例如当我们处理桶{20000}时,我们可通过第31至32行产生一条比较指令“if (a !=20000)
goto BB19;”,而当我们面对{50}或{1,2}时,则需要通过第34至39行产生形如“if (a < 50)goto BB4; BB1: if (a > 50) gotoBB8;”这样的中间代码。
图5.3.6 TranslateSwitchBuckets()
图5.3.6第40至50行用于产生跳入相应case语句的代码,例如上述用于跳入case 50的指令“BB2: goto BB17;”,当跳转表的大小为1时,我们只有一个跳转目标,通过第49行产生一条无条件跳转指令即可;否则在第43至46行,通过跳转表来进行跳转,例如我们在图5.3.4 中为桶{1,2}产生以下跳转代码:
BB6:
t0 : a - 1;
goto (BB11,BB13,)[t0]; //跳往case 1或case 2
图5.3.6第52至53递归地调用TranslateSwitchBuckets,对位于当前桶的左侧的各个桶进行处理,而第54至55行则递归地对右侧进行翻译。
Switch语句的翻译是所有控制流语句中最复杂的。为了讨论的完整性,我们再给出UCC编译器中for语句、while语句和do语句的翻译方案,如图5.3.7第2至30行所示,其他编译器的翻译方案可能与此有所不同,但都要实现相同的语义。图5.3.7第32至41行用于翻译break语句,break可跳出循环语句或switch语句,第36和38行通过产生无条件跳转指令跳出这些语句。第42至47行用于翻译continue语句,这可通过在第45行产生跳转语句,跳入循环语句的contBB基本块来实现,例如图5.3.7第6行、第15行和第23行所示的contBB。第48至56行用于处理return语句,如果有返回值,我们可在第52行调用TranslateExpression函数来翻译相应的表达式,并把结果存于RET指令中。在UCC编译器中,RET指令并不改变控制流,UCC编译器为简化处理,使每个待翻译的函数都只有唯一的入口基本块entryBB,及唯一的出口基本块exitBB,第54行会产生无条件跳转指令跳入exitBB基本块,即相当于要从函数返回。
图5.3.7 循环语句的翻译方案
至此,我们完成了对语句的翻译,为了得到运行时更高效的代码,我们还需要对已生成的中间代码进行优化,例如在图5.3.4第49至52行,我们可以看到以下代码:
b = 50;
goto BB19;
BB19:
b = 60;
经过优化,我们可删除其中无用的跳转指令“goto BB19;”,从而得到:
b = 50;
BB19:
b = 60;
优化是编译相关领域研发的热点,与优化相关的理论和技术不仅可用于编译器,还经常被用于信息安全等领域。UCC编译器只进行了一些简单的代码优化,我们会在下一节中进行讨论。