12.5. 循环展开

循环展开是一种面向循环体的优化方法。它通过重复展开循环体中的指令,减少循环迭代次数,甚至完全取消循环,从而降低循环变量更新和条件判断反复执行带来的开销。

例如,在10.4.2小节的循环指令中,fld.ffadd.ffsd.f 直接完成数据处理,而 addibnez 属于循环控制开销。通过展开循环,可以减少甚至消除这些控制指令的执行次数。当循环次数较少时,例如只循环3次,可以将循环完全展开,写成如下形式:

i1             fld.f    fa0,   0(r1)
i2             fadd.f  fa2,   fa0,   fa1
i3             fsd.f   fa2,   0(r1)
i4             fld.f    fa0,   4(r1)
i5             fadd.f  fa2,   fa0,   fa1
i6             fsd.f   fa2,   4(r1)
i7             fld.f    fa0,   8(r1)
i8             fadd.f  fa2,   fa0,   fa1
i9             fsd.f   fa2,   8(r1)

展开前循环体包含5条指令,执行3轮共需15条指令。完全展开后,只需9条指令即可完成相同功能。

展开后的指令还可以继续做调度优化。通过改用不同寄存器并重新排列指令,可以降低数据相关带来的等待,更充分地利用流水线。优化后的序列如下:

i1             fld.f      fa0,   0(r1)
i4             fld.f      fa3,   4(r1)
i7             fld.f      fa4,   8(r1)
i2             fadd.f    fa2,   fa0,   fa1
i5             fadd.f    fa5,   fa3,   fa1
i8             fadd.f    fa6,   fa4,   fa1
i3             fsd.f     fa2,   0(r1)
i6             fsd.f     fa5,   4(r1)
i9             fsd.f     fa6,   8(r1)

在编译器优化中,针对迭代次数较大的循环通常会设置最大展开次数,常见取值为4次、8次或16次。仍以10.4.2小节的指令为例,若迭代次数为1000,并按4次展开,则可写为:

Loop:      fld.f      fa0,   0(r1)
           fld.f      fa3,   4(r1)
           fld.f      fa4,   8(r1)
           fld.f      fa5,   12(r1)
           fadd.f     fa2,   fa0,   fa1
           fadd.f     fa6,   fa3,   fa1
           fadd.f     fa7,   fa4,   fa1
           fadd.f     fa8,   fa5,   fa1
           fsd.f      fa2,   0(r1)
           fsd.f      fa6,   4(r1)
           fsd.f      fa7,   8(r1)
           fsd.f      fa8,   12(r1)
           addi.w     r1,    r1,    -16
           bnez       r1,    Loop          

由于一次循环处理4组数据,addi.w 中 r1 的更新量由原来的 -4 调整为 -16。此时 addibnez 没有完全消失,但循环次数明显减少,总执行指令数也随之下降。

循环展开能减少循环控制开销,但会扩大代码体积,并可能降低指令 Cache 命中率。CPU 能在指令 Cache 中找到所需指令称为命中,反之为不命中;命中率是命中指令数占全部执行指令数的比例。因此,展开次数不宜过大,通常16次可视为较高上限。另一个限制来自寄存器数量。循环展开后,如果仍有空闲寄存器,就可以借助这些寄存器削弱数据依赖,并对展开后的指令继续调度优化;如果展开次数过多而寄存器不足,就需要把部分寄存器内容保存到栈上再恢复,或接受由数据依赖造成的部分性能损失。