对于在学校的任务,我正在对一大堆数字进行密集操作。在对整个阵列上运行的单线程版本进行基准测试并将我的结果与我的同学进行比较时,我发现了一些奇怪的行为。
功能如下:
int compute (char a[], int start, int end) {
int sum = 0;
int min = a[start];
int max = a[start];
for (int i = start; i < end; i++) {
if (a[i] > max) max = a[i];
if (a[i] < min) min = a[i];
int cube = a[i] * a[i] * a[i];
sum += cube;
}
return sum;
}
但是我同学的课程一直运行得更快,通常要快得多。他的代码是相同的,除了循环体中指令的顺序:
for (int i = start; i < end; i++) {
int cube = a[i] * a[i] * a[i];
sum += cube;
if (a[i] > max) max = a[i];
if (a[i] < min) min = a[i];
}
这是将每个版本的运行时与大小为1,000,000,000的输入数组(使用随机带符号字节初始化)进行比较的输出:
Min/max first:
sum = 5445493143089, min = -128, max = 127
Completed in 1.050268 sec
Product-sum first:
sum = 5445493143089, min = -128, max = 127
Completed in 1.010639 sec
我们已经检查了两个版本的生成的程序集,并注意到存在相同的指令,只是以不同的顺序排序。据我所知,这不应该像它那样有效,但我可能是错的。 (我们也注意到使用的寄存器差异很大,但我特别怀疑应该有效。)
在编译C(-std=c11
)和C ++(-std=c++11
)时遇到此行为。
为什么这些行的顺序会严重影响顺序程序的行为?我们还对操作的并行版本进行基准测试,相反,它的行为几乎没有变化。我将内存重新排序视为可能的罪魁祸首,但这似乎不是问题,因为并行版本几乎不受影响(并且分区中没有任何重叠)。
Intensive back-to-back tests 演示了这种行为。产品总和总是快于最小/最大,即使在交替和允许缓存。
答案 0 :(得分:8)
如果我们将明确的跳转放入代码中,您可以看到最后一个带有条件的跳转可以避免大部分时间跳转。这类似于编译器实际生成的代码。
第一种形式,最小/最大第一:
int i = lo;
goto start;
loop:
i++;
start:
if (!(i < hi)) goto end;
if (!(a[i] > ret.max)) goto label1;
ret.max = a[i];
label1:
if (!(a[i] < ret.min)) goto label2;
ret.min = a[i];
label2:
long long square = a[i] * a[i];
ret.sum += square;
goto loop;
end:
第二种形式,最小/最后一次:
int i = lo;
goto start;
loop:
i++;
start:
if (!(i < hi)) goto end;
long long square = a[i] * a[i];
ret.sum += square;
if (!(a[i] > ret.max)) goto label1;
ret.max = a[i];
label1:
if (!(a[i] < ret.min)) goto loop;
ret.min = a[i];
goto loop;
end:
答案 1 :(得分:4)
可能就像处理器跳转预测在循环底部的条件跳转更好地工作一样简单......