完全透明,这是一项家庭作业。
我在弄清楚如何优化此代码时遇到了一些麻烦......
我的讲师继续展开和拆分,但似乎都没有大大减少执行代码所需的时间。任何帮助将不胜感激!
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.
}
答案 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)
除了作弊*之外,这个内部循环基本上是不可优化的。因为您必须获取所有数组元素并执行所有添加。
循环体执行:
j
; array[j]
; j
。如上所述,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;
}
然后,编译器将应用上述所有优化。