垃圾内容,读书笔记,勿看
本书重在Computer Architecture,所以对compiler optimization没有做重点介绍,本章节算是对一些compiler optimization进行了科普。理解本章节,可以按照“计算机体系结构就是trade-off”这个主旨进行。囿于现实因素,存在很多限制,而编译优化就是在夹缝中就生存,在诸多条条框框中求得一个近优解。
3.2 Basic Compiler Techniques for Exposing ILP
类似于图灵机,由于现实的束缚,不可能存在无限长“纸带”的机器,就连地址无限长都做不到。现实附加了很多约束,例如资源有限的DRAM和磁盘。约束分为两类,理论约束与实现约束。
for (i = 999; i >= 0; i = i -1)
x[i] = x[i] + s;
上述代码的理论约束有二:
- 必须在
x[999]
->x[0]
每个元素上面加上s
。
这意味着,
- 你按照什么样的形式遍历
x
无所谓,例如无论你是一次迭代4个元素,还是一次迭代999个元素; - 你按照什么样的形式加
s
也无所谓,例如你是+ (s + 1) - 1
还是+ (s - 1) + 1
都无所谓。
这是理论上的upper bound,我们在实现的时候不可能改变语义,法无禁止皆自由(只要“能力”够强,随便你玩儿😛)。
但是现实也有一个暂时的upper bound,例如在intel最新的芯片上,向量化的能力只能到512bits,一次只能处理64个bytes,“暂时”是说未来的技术会继续发展,最终触碰到物理基线(我这就有点儿瞎扯淡了)。现实的芯片有诸多类似的限制,例如branch miss带来的影响,cache miss带来的影响,DRAM与cpu的速度差等等。
所以编译器的作用就是基于程序语义,来尽可能的逼近“现实极限”。例如有cache、register以及pipeline三个约束的话,那么编译器就是在这个三个约束的情况下去优化程序,关键是这三个约束还不是正交的。在A约束的情况下优化了性能,但是由于B约束的原因,性能可能会恶化。
本书以RISC-V为例,生成的naive代码假设如下所示,可以看到这里的约束有指令的latency以及指令间的依赖,下面的代码需要8个时钟周期,3次stall。
注:摘自原书
单看这个loop body,遵循理论依赖的前提下,优化的空间不多了。可以将addi x1, x1, -8
指令调度到fld
指令的后面来减少一个stall。基于前面的分析,代码并没有规定遍历与赋值的具体规则和实现。基于此,我们可以一次处理两个element。
for (i = 999; i >= 0; i = i -2) {
x[i] = x[i] + s;
x[i - 1] = x[i - 1] + s;
}
x[0] = x[0] + s;
整个iteration的次数降下来了,这样的话,我们就可以节省loop overhead相关的指令,例如addi x1, x1, -8
以及bne
指令。
Loop:
# ----- 1st iteration -------
fld f0,0(x1)
stall
fadd.d f4,f0,f2
stall
stall
fsd f4,0(x1) # drop addi & bne
# ----- 2nd iteration -------
fld f6,-8(x1)
stall
fadd.d f8,f6,f2
stall
stall
fsd f8,-8(x1) # drop addi & bne
# ----- 3rd iteration -------
fld f0,-16(x1)
stall
fadd.d f12,f0,f2
stall
stall
fsd f12,-16(x1) # drop addi & bne
# ((x1 - 8) - 8) -> x1 - 16
# ----- 4th iteration -------
fld f14,-24(x1)
stall
fadd.d f16,f14,f2
stall
stall
fsd f16,-24(x1) # drop addi & bne
# (((x1 - 8) - 8) - 8) -> x1 - 24
# loop overhead
addi x1,x1,32
bne x1,x2,Loop
首先把loop控制相关的指令给删除了,这都是为了便于程序员理解的的俗物,快给删掉,编译器帮你安排的明明白白的。在满足理论约束的情况下,为了进一步提升性能,出现了指令调度的技术,loop unroll之后可以在更大范围内进行指令调度,进一步的逼近现实upper bound。但是loop unroll增加了寄存器的使用,并且提高了code size,所以loop unroll的缺点也很明显,这个在最后面再说。
经过指令调度后,减少了stall,同时也可以enable向量化优化。
Loop:
fld f0,0(x1)
fld f6,8(x1)
fld f0,16(x1)
fld f14,24(x1)
fadd.d f4,f0,f2
fadd.d f8,f6,f2
fadd.d f12,f0,f2
fadd.d f16,f14,f2
fsd f4,0(x1)
fsd f8,8(x1)
fsd f12,16(x1)
fsd f16,8(x1)
addi x1,x1,32
bne x1,x2,Loop