我尝试了两种几乎相同的计算前缀和的方法,编译后发现它们有明显的区别,编译选项是-O2
。
不同的编译结果导致它们的运行时间相差4倍。
第一个:
#include <numeric>
#include <algorithm>
int main() {
unsigned a[5000];
std::iota(a, a + 5000, 0);
for (int k = 0; k < 1'000'000; k++)
for (int i = 1; i < 5000; i++)
a[i] += a[i - 1];
return *std::min_element(a, a + 5000);
}
字符串
第二个:
#include <numeric>
#include <algorithm>
int main() {
unsigned a[5000];
std::iota(a, a + 5000, 0);
for (int k = 0; k < 1'000'000; k++)
for (int i = 0; i + 1 < 5000; i++)
a[i + 1] += a[i];
return *std::min_element(a, a + 5000);
}
型
In Compiler Explorer
编译器中出现此异常的原因是什么?
1条答案
按热度按时间p4rjhz4m1#
在版本2中,我们看到GCC已经做到了这一点,
字符串
所以累加
edx
中的值,向其中添加一个新元素,并将累加的和存储到内存中。这很好,这里没有问题。在版本1中,我们看到GCC已经做到了这一点,
型
在这里,先前存储的部分和再次加载,然后将其添加到当前元素中。
这并不好,有一个通过内存的循环依赖,将存储+重新加载的延迟放在关键路径上。在各种CPU上,每次迭代可能是5或6个周期。在版本2中,类似的依赖通过
edx
而不是通过内存,这在每个CPU上都非常有效,让循环每次迭代执行接近1.5个周期。基于这种效果,预期大约4倍的性能差异。在一些较新的CPU上,如果CPU可以在这种情况下使用内存重命名(零延迟存储来加载转发),它可能不会那么糟糕。然而,关于英特尔冰湖,Agner Fog写道:
根据我的测试,在以下情况下快进不起作用:
...
我们这里有一个读-修改-写指令,所以这可能不好。
对于AMD风格的内存重命名,显然内存操作数必须是精确匹配的(不仅仅是计算的地址,还有它的指定方式),我们在这里没有,所以它可能也不好。
我不知道为什么GCC会以如此不同的方式编译这两个版本。也许是有原因的,我只是想不出来,但也许只是运气不好。