在语义分析之后,就会把抽象语法树转化为类似汇编的中间代码序列。
编译器的中间代码与汇编的主要区别就是,汇编是以寄存器表示变量,而中间代码可以直接使用变量名。
例如,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 条评论) “” |