编译器后端的核心就是各种优化。
这些优化,都是基于源代码所表达的程序流程图的。
源代码是由顺序语句、if / else语句、for / while循环语句构成的复杂逻辑结构。
switch / case语句的作用类似if / else语句,在编译器后端看来两者几乎没有区别。
goto语句的作用要复杂一些,但大多数情况下类似循环语句里的break。
int f()
{
int a = 5, b = 3, c = a / b; // 可知 c = 1
if (c) //成立
c += a; // 可得 c = 6
else
c -= b; // 不会运行到这里
return c; // 实际就是 return 6
}
这种代码如果深度优化的话,可以优化成一个常量,这就是常量传播。
这个函数的实际效果就是return 6,汇编码是mov 6, eax,有兴趣的可以用gcc -O3试验一下。
scf里并没有添加常量传播的优化,所以还是会生成一堆的冗余代码。
上述代码的流程图如下,编译器后端看到的并没有if / else语句,而是这么一个流程图。
没有分支跳转的语句块组合在一起,就是一个个的基本块(basic block)。
不同的基本块之间通过分支跳转来连接,形成了程序的流程图。
if / else语句属于典型的分支跳转语句,在编译器的后端都会转化成比较和跳转。
在这个流程图上,对变量的赋值必然会修改变量的值。
如果赋值的是常量,那么赋值之后变量的值就是这个常量。
这个赋值会影响到当前基本块的后续语句,和后续的基本块里的所有语句。
也就是说,a = 5和b = 3这两行赋值会影响到c = a / b和if (c),以及后续的c +=a或c -= b,以及最后的return c。
既然赋值的是常量,那么提前把它计算出来,就可以节省运行时间,提高程序效率。
具体的计算过程,已经在上面的源码里加了注释了。
编译器只要记录每一行代码之后的变量状态,就可以实现常量传播的优化:
1,每个赋值语句都会修改变量的值,记录下来。
2,如果是其他运算语句,(沿着流程图)往前查看使用的变量有没有被常量赋值,
3,如果有,把运算结果计算出来又获得一个常量,继续记录下来。
4,只要沿着流程图按照步骤1、2、3遍历一遍,就可以优化掉所有的常量运算了。
在确定c = a / b = 1之后,if (c)就必然为真,所以else部分的c -= b就是死代码。所以常量传播也可以导致死代码消除。
当消除了else部分的死代码之后,if语句也就变成了顺序语句了,分支跳转也就可以去掉了。
我在scf框架里对指针的指向分析,实际上就是对指针变量的常量传播优化。
给常量传播下个定义:常量从对变量的赋值开始,沿着程序流程图的影响扩散过程,就是常量传播。
留言与评论(共有 0 条评论) “” |