使这个for循环更有效率?

时间:2017-12-12 05:45:35

标签: c for-loop gcc optimization

    • 编辑 - - 此代码将在关闭优化的情况下运行

完全透明,这是一项家庭作业。

我在弄清楚如何优化此代码时遇到了一些麻烦......

我的讲师继续展开和拆分,但似乎都没有大大减少执行代码所需的时间。任何帮助将不胜感激!

for (i = 0; i < N_TIMES; i++) {

    // You can change anything between this comment ...

    int     j;

    for (j = 0; j < ARRAY_SIZE; j++) {
        sum += array[j];
    }

    // ... and this one. But your inner loop must do the *same
    // number of additions as this one does.

}

3 个答案:

答案 0 :(得分:3)

假设您在运行时对sum添加相同数量的添加(而不是源代码中相同数量的添加),则展开可能会给您类似的内容:

for (j = 0; j + 5 < ARRAY_SIZE; j += 5) {
    sum += array[j] + array[j+1] + array[j+2] + array[j+3] + array[j+4];
}
for (; j < ARRAY_SIZE; j++) {
    sum += array[j];
}

或者,由于您每次通过外部循环添加相同的值,因此您不需要处理它N_TIMES次,只需执行以下操作:

for (i = 0; i < N_TIMES; i++) {
    // You can change anything between this comment ...
    int     j;
    for (j = 0; j < ARRAY_SIZE; j++) {
        sum += array[j];
    }
    sum *= N_TIMES;
    break;
    // ... and this one. But your inner loop must do the *same
    // number of additions as this one does.
}

这要求sum的初始值为零,这很可能但是你的问题实际上并没有强制要求这个,所以我把它作为这个方法的先决条件。< / p>

答案 1 :(得分:-1)

除了作弊*之外,这个内部循环基本上是不可优化的。因为您必须获取所有数组元素并执行所有添加。

循环体执行:

  1. j;
  2. 上的条件分支
  3. 获取array[j];
  4. 积累到标量变量;
  5. j
  6. 的增量

    如上所述,2.到4.是不可避免的。然后你所能做的就是通过循环展开来减少条件分支的数量(这将条件分支转换为无条件分支,代价是迭代次数变得固定)。

    你没有看到很大的不同并不奇怪。现代处理器具有“循环感知”,这意味着分支预测可以很好地适应这种循环,因此分支的成本非常低。

    <强>作弊:

    正如其他人所说,你可以完全绕过外环。这只是在利用练习陈述中的一个缺陷。

    由于必须关闭优化,因此也应禁止使用内联汇编,编译指示,向量指令或内在函数(不提及自动并行化)。

    有可能在很长的时间内打包两个整数。如果总和没有溢出,您将一次执行两次添加。但这是合法的吗?

    有人可能会想到一种有利于缓存利用率的访问模式。但是这里没有希望,因为数组在每个循环中都被完全遍历,并且不可能重用所获取的值。

答案 2 :(得分:-2)

首先,除非您使用-O0进行显式编译,否则您的编译器可能已经比您预期的更加优化了这个循环。

包括展开,以及展开矢量化等等。尝试手工优化这是你永远不应该做的事情,绝对不会这样做。最多,您将成功地使代码更难以阅读和理解,而很可能甚至无法在性能方面匹配编译器。

至于为什么没有可衡量的收益?可能是因为你已经遇到了瓶颈,即使是“非优化”版本。对于比处理器缓存更大的ARRAY_SIZE,即使编译器优化版本已经受到内存带宽的限制。

但是为了完整性,我们假设您没有遇到这个瓶颈,并且您实际上已经几乎关闭了优化(因此不超过-O1),并为此进行了优化。

for (i = 0; i < N_TIMES; i++) {

    // You can change anything between this comment ...

    int j;
    int tmpSum[4] = {0,0,0,0};

    for (j = 0; j < ARRAY_SIZE; j+=4) {
        tmpSum[0] += array[j+0];
        tmpSum[1] += array[j+1];
        tmpSum[2] += array[j+2];
        tmpSum[3] += array[j+3];
    }

    sum += tmpSum[0] + tmpSum[1] + tmpSum[2] + tmpSum[3];

    if(ARRAY_SIZE % 4 != 0) {
        j -= 4;
        for (; j < ARRAY_SIZE; j++) {
            sum += array[j];
        }
    }

    // ... and this one. But your inner loop must do the *same
    // number of additions as this one does.

}

对于较小的array,几乎只有一个因素可能会降低性能。

不是循环的开销,因此使用现代处理器进行简单的展开是毫无意义的。甚至不打扰,你不会打败分支预测。

但两条指令之间的延迟,直到一条指令写入的值可能被下一条指令再次读取仍然适用。在这种情况下,sum会不断被写入并重新读取,即使sum缓存在寄存器中,此延迟仍然适用,处理器管道必须等待。

解决这个问题的方法是同时进行多个独立的添加,最后只是将结果组合起来。顺便说一句,这也是大多数现代编译器都知道如何执行的优化。

最重要的是,你现在还可以用向量指令表达第一个循环 - 再一次也是编译器会做的事情。此时您再次遇到指令延迟,因此您可能不得不再引入一组临时值,以便您现在有两个独立的加法流,每个都使用向量指令。

为什么要求至少为-O1?因为否则编译器甚至不会将tmpSum放在寄存器中,或者会尝试表示例如array[j+0]作为一系列指令,用于首先执行添加,而不是仅使用单个指令。在这种情况下很难进行优化,而无需直接使用内联汇编。

或者,如果你只是觉得(合法)作弊:

const int N_TIMES = 1000;
const int ARRAY_SIZE = 1024;
const int array[1024] = {1};
int sum = 0;

__attribute__((optimize("O3")))
__attribute__((optimize("unroll-loops")))
int fastSum(const int array[]) {
      int j;
      int tmpSum;

      for (j = 0; j < ARRAY_SIZE; j++) {
          tmpSum += array[j];
      }
      return tmpSum;
}
int main() {
    int i;
    for (i = 0; i < N_TIMES; i++) {
        // You can change anything between this comment ...


        sum += fastSum(array);

        // ... and this one. But your inner loop must do the *same
        // number of additions as this one does.

    }
    return sum;
}

然后,编译器将应用上述所有优化。