编译器优化那些事儿(2):常量传播

0.基础知识盘点

  • 基本块 (Basic Block)
    一个基本块内的指令,处理器会从基本块的第一条指令顺序执行到基本块的最后一条指令,中间不会跳转到其它地方去,也不会有其它地方跳转到基本块的非首条指令上来。

  • 控制流图 (Control Flow Graph)
    控制流图的节点是基本块,边代表基本块之间的跳转。基本块A到基本块B有一条边,表示基本块A的最后一条指令是一个跳转指令,跳转到了基本块B的第一条指令;或者基本块A的最后一条指令执行完可以顺序的走向基本块B的第一条指令。

  • SSA (Static Single Assignment)
    静态单赋值,指程序的一种表示形式,在该形式中变量只被赋值一次,如果变量需要更新,会使用一个新的变量。决定使用哪个分支处的变量,会使用Φ函数。
    例如

    min = a;
    if(min > b)
         min = b;
    return min;
    

    转换为SSA形式为:

    min = a;
    if(min > b)
        min2 = b;
    return Φ(min, min2)
    
  • 局部优化
    指基本块内,跨指令的优化。

  • 全局优化
    过程(函数)内的,跨基本块的优化。

  • 过程间的优化
    跨过程(函数),编译单元的优化。

1.什么是常量传播

首先来认识一下什么是常量传播,常量传播也叫常量折叠,但有些资料中对它们的定义又是区分开来的。下面来看看它们分别是什么?

常量传播,顾名思义,就是把常量传播到使用了这个常量的地方去,用常量替换原来的变量。

x = 3;
y = 4;
z = x + y;

->

x = 3;
y = 4;
z = 3 + 4;

什么是常量折叠?常量折叠就是当运算符左右两边都是常数时,把结果计算出来替代原来的那部分表达式。

z = 3 + 4;

–>

z = 7;

现在的常量传播优化技术能同时实现上面介绍的传播和折叠的功能,所以现在通常不对它们加以区分,本文后续就把常量传播和折叠统称为常量传播了。

为什么会有常量?

程序中的常量来源有3种:

  1. 程序员书写的,比如magic number
  2. 宏定义展开后带来的,这种情况在大型工程文件中非常普遍
  3. 在程序优化过程中由其它的优化技术带来的常数

为什么要进行常量传播优化?

从上面简单的例子中可以看到,常量传播可以把原本在运行时的计算转移到编译时进行,减小了程序运行时的开销。同时常量传播还有助于实现其它的优化,比如死代码消除。

如何实现?

前面介绍的常量传播的例子非常简单而且直观。如果程序的控制逻辑比较复杂时,判断一个变量是否是常数,就不是一件简单的事了,需要借助数据流的分析才能判断某个变量是否是常量。而且这个判断是保守的,即不能充分证明某个变量是常量的话就认为它是变量。这种保守的分析结果是可以接受的,因为我们优化程序的时候在性能提升与程序语义保证时优先选择保证程序语义不变。

下面我们先初步学习一下数据流分析。

2.数据流分析

数据流分析常常是为了实现全局优化、过程间优化,或者程序静态分析而进行的分析技术。分析得到的信息可以支撑各种优化技术的落实。

考虑下面这条指令,我们能对它做什么优化呢?

z = x + y;

+ 代表各种有效的运算符, 比如±*/等

最基本的,有下面3种假设:

  1. 如果x或者y是常量,我们可以做常量传播, 用常量值替代变量x, y。 如果xy都是常量,可以在编译时刻把x+y计算出来,并赋值给z,这样就不会在运行时做这样的计算了。
  2. 如果x+y在前面已经被计算过了,而且xy再没有被赋值过(是一个可用表达式),那么此处就可以将上次计算过的值直接赋值给z, 省去再次计算的代价,这样的优化称为公共子表达式的消除。
  3. 如果z从这里开始到程序结尾再没有被使用(z是不活跃变量),或者是有使用,但使用前被重新赋值了(z是不活跃变量),那么这个赋值语句是没用的,可以删除掉这条语句。

可以看到对每一种可能的优化,都需要一定的依据。这些依据就需要进行数据流的分析来获取。

下面我们介绍一下非常基础的3种数据流模式。

  1. 到达定值
    告诉我们在一个程序点上,过程(函数)里的变量分别是在什么位置被定值(赋值)的。常用在常量传播,复制传播上。
  2. 可用表达式
    告诉我们在一个程序点上,可用的表达式有哪些。常用在公共子表达式消除。
  3. 活跃变量
    告诉我们在一个程序点上,活跃变量(将来还会用到的变量)有哪些。常用于优化寄存器分配,删除死代码。

2.1 到达定值

2.1.1 转移函数

程序中的每一条语句都会对程序的状态产生影响,程序的状态包括了寄存器的值、内存的值、读写的文件等。对于特定的数据流分析,我们只关心对我们分析或者程序优化有用的那部分内容。比如对到达定值分析,我们只跟踪变量的定值情况,对可用表达式的分析,我们跟踪表达式的生成以及表达式分量的赋值情况,对于活跃变量我们关心变量的赋值和使用情况。

我们用转移函数来表示程序语句对程序状态的影响:
O U T = f d ( I N ) OUT=f_d(IN) OUT=fd(IN)
f d f_d fd 是语句d的转移函数。IN是语句d前面的程序状态,OUT是语句d之后的程序状态。

同样的,基本块对程序状态也有影响,基本块也有转移函数:
O U T = f B ( I N ) OUT = f_B(IN) OUT=fB(IN)
f B f_B fB 是基本块B的转移函数,IN是基本块B之前的程序状态(也可表示为IN[B]),OUT是基本块B结束后的程序状态(也可表示为OUT[B])。

不同的分析目的,转移函数不同,对于到达定值分析,如果遇到下面的一条语句

d: u = v + w

我们说这个语句生成了一个对变量u的定值d,同时杀死了其它对变量u的定值。记为: g e n = { d } gen = \{d\} gen={ d}, k i l l = { 其它对 u 的所有定值 } kill = \{其它对u的所有定值\} kill={ 其它对u的所有定值}

对上面的赋值语句来说,它的转移函数就是:
f d ( x ) = g e n d ∪ ( x − k i l l d ) f_d(x) = gen_d∪(x-kill_d) fd(x)=gend(xkilld)

其中 g e n d = { d } gen_d= \{d\} gend={ d}, k i l l d = { 其它对 u 的所有定值 } kill_d = \{其它对u的所有定值\} killd={ 其它对u的所有定值}

基本块的转移函数由基本块的每一个语句的转移函数组合构成。比如2个语句组成的基本块的到达定值转移函数是
f B ( x ) = f 2 ( f 1 ( x ) ) = g e n 2 ∪ ( g e n 1 ∪ ( x − k i l l 1 ) − k i l l 2 ) = ( g e n 2 ∪ ( g e n 1 − k i l l 2 ) ) ∪ ( x − ( k i l l 1 ∪ k i l l 2 ) ) \begin{array}{l} f_B(x) &=& f_2(f_1(x)) \\ &=& gen_2∪(gen_1∪(x - kill_1)-kill_2) \\ &=& (gen_2∪(gen_1-kill_2))∪(x-(kill_1∪kill_2)) \\ \end{array} fB(x)===f2(f1(x))gen2(gen1(xkill1)kill2

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值