首先,除非您使用-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;
}
然后编译器将应用上述几乎所有的优化。