循环展开

循环展开

Content #

代码优化技巧“循环展开”。让我们先来看一段代码:

#define LEN 4096
int data[LEN] = { ... };
int foo(void) {
  int acc = 1;
  for (int i = 0; i < LEN; ++i) {
    acc = acc * data[i];
  }
  return acc;
}

在这段代码中,我们定义了一个名为 data 的全局整型数组,并在其中存放了若干个值。而函数 foo 则主要用来计算该数组中所有数字的乘积之和。

此时,如果我们在 main 函数中调用函数 foo,CPU 在实际执行它的代码时, for 循环的每一轮都会产生两个数据相关:循环控制变量 i 的下一个值依赖于本次循环变量 i 在经过自增运算后得到的结果值。同样地,计数变量 acc 的下一个值也依赖于该变量在当前循环中经过乘积计算后的结果值。

而这两个数据相关会导致 CPU 无法提前计算下一轮循环中各个参与变量的值。而只有在寄存器写回,或内存访问阶段执行完毕,也就是变量 acc 和 i 的值被最终更新后,CPU 才会继续执行下一轮循环。

那么,应该如何优化这个过程呢?我们直接来看优化后的代码:

#define LEN 4096
int data[LEN] = { ... };
int foo(void) {
  int limit = LEN - 1;
  int i;
  int acc0 = 1;
  int acc1 = 1;
  for (i = 0; i < limit; i += 2) {  // 2x2 loop unrolling.
    acc0 = acc0 * data[i];
    acc1 = acc1 * data[i + 1];
  }
  for (; i < LEN; ++i) {  // Finish any remaining elements.
    acc0 = acc0 * data[i];
  }
  return acc0 * acc1;
}

可以直观地看到,参与到程序执行的局部变量变多了。而这里的主要改动是,我们为函数 foo 中的循环结构应用了 2x2 循环展开。

循环展开这种代码优化技术,主要通过增加循环结构每次迭代时计算的元素个数,来减少循环次数,同时优化 CPU 的指令集并行与流水线调度。而所谓的 2x2 ,是指在优化后的代码中,循环的步长变为了 2,且循环累积值被分别存放在两个独立变量 acc0 与 acc1 中。

循环展开带来的最显著优化,就是减少了循环的迭代次数。使用多个独立变量存储累积值,各个累积值之间就不会存在数据相关,而这就增大了 CPU 多个执行单元可以并行执行这些指令的机会,从而在一定程度上提升了程序的执行效率。

需要注意的是,循环展开一方面可以带来性能上的提升,另一方面它也会导致程序代码量的增加,以及代码可读性的降低。并且,编译器在高优化等级下,通常也会对代码采用隐式的循环展开优化。因此,在大多数情况下,我们并不需要手动地改变代码形式来为它应用循环展开,除非是在那些你确定编译器没有进行优化,并且手动循环展开可以带来显著性能提升的情况下。

Viewpoints #

From #

19|极致优化(下):如何实现高性能的 C 程序?