技术文章:编译器后端优化,跳转导致的死代码消除

语义分析之后,就会把抽象语法树转化为类似汇编中间代码序列。

编译器的中间代码与汇编的主要区别就是,汇编是以寄存器表示变量,而中间代码可以直接使用变量名

例如,a = b + c;

在生成汇编之前要先给这3个变量分配寄存器,例如a = eax, b = ebx, c = ecx,那么汇编码就是:

mov eax, ebx,

add eax, ecx.

英特尔的指令只有2个操作数,a = b + c要进行两步运算:a = b和a += c,生成2条汇编。

如果是中间代码的话,可以直接这么写:add a, b, c,让它带3个操作数。

中间代码不受制于CPU指令集,比汇编稍微灵活一点。

在生成中间代码之后,面临的第一个优化就是:跳转导致的死代码消除

源代码里有if else / while / for / break / continue / goto等语句的时候,生成的中间代码里就有跳转

例如:

if (i < j) i++;

中间代码就是:

cmp i, j,

jge end,// 跳转

inc i,

end

当跳转是绝对跳转(jmp指令)的时候,它后面的代码是不能从它前面的代码顺序运行到的。jmp指令相当于一个代码屏障。

如果jmp后面的代码也不能从其他地方跳转过来,那么它就是死代码

要从其他地方跳转过来,必须有一个表示代码位置标号,否则就是死代码。

例如:

jmp end,

inc i, // 这里就是死代码

end.

这条i++永远没法运行到,因为它前面有个绝对跳转,又没有其他代码可以跳转到它这里。

for (int i = 5; i < 10; i++) {

if (i > 4) break;

i--; // 这里就是死代码

}

i的初始值是5,它肯定是大于4的,所以if (i > 4)必然会导致break,从而跳过后面的i--。

所以i--也是死代码。

如果跳转的目标位置也是一条跳转指令,就会导致死代码的级联消除

例如:

if (flag) goto L1;

L1:goto L2;

L2:...

这种代码翻译成中间代码就是:

test flag, flag

jne L1

L1: jmp L2

L2: ...

这连续2次的跳转可以用1次跳转代替,if (flag) goto L2;

也就是把jne L1和jmp L2替换成jne L2。

任何跳转(jcc dst0)和绝对跳转(jmp dst1)组合的时候,只要把绝对跳转的目标位置(dst1)放到jcc的后面就行:jcc可以是条件跳转,也可以是绝对跳转。

反过来也一样

test flag, flag

jmp L1

L1: jne L2 //这里的状态,与前面的test flag,flag结束时是一样的,跳转本身不改变状态。

L2: ...

所以上面的中间代码也可以替换成:

test flag, flag

jne L2.

消除掉多余的跳转,以及绝对跳转后面的无效代码,是编译器后端优化的第一步。

这个优化完成之后就可以根据剩余的跳转情况,把中间代码序列划分成一个个的基本块

基本块的划分原则在“编译原理”(龙书)上有讲。

scf编译器框架的划分原则如下:

1,每个函数的开头是一个基本块的起点,

2,每一个跳转是一个基本块的起点,

3,每一个跳转的目标位置是一个基本块的起点,

4,每一个跳转的下一个位置是一个基本块的起点,

5,我认为需要划分为一个基本块的其他情况[捂脸]

基本块的划分都是以函数为单位的,每个函数包含多个基本块。

基本块内部的代码都是顺序运行的,不包含跳转。

函数内的基本块之间的关系就是程序的流程图,它是通过跳转来连接的。

跳转导致的死代码消除,以及基本块的划分,都在文件scf/core/scf_3ac.c里,有兴趣的可以看看[呲牙]

发表评论
留言与评论(共有 0 条评论) “”
   
验证码:

相关文章

推荐文章