gcc优化标志-O3使代码比-O2慢
gcc optimization flag -O3 makes code slower than -O2
我找到了这个主题为什么处理排序数组比处理未排序数组更快。并尝试运行此代码。我发现了一些奇怪的行为。如果使用-O3
优化标志编译此代码,则需要2.98605 sec
才能运行。如果使用-O2
进行编译,则需要1.98093 sec
。我试着在同一环境中的同一台机器上运行这段代码好几次(5或6次),我关闭了所有其他软件(chrome、skype等)
gcc --version
gcc (Ubuntu 4.9.2-0ubuntu1~14.04) 4.9.2
Copyright (C) 2014 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
请你向我解释一下为什么会发生这种情况?我阅读了gcc
手册,发现-O3
包括-O2
。谢谢你的帮助。
p.S.添加代码
#include <algorithm>
#include <ctime>
#include <iostream>
int main()
{
// Generate data
const unsigned arraySize = 32768;
int data[arraySize];
for (unsigned c = 0; c < arraySize; ++c)
data[c] = std::rand() % 256;
// !!! With this, the next loop runs faster
std::sort(data, data + arraySize);
// Test
clock_t start = clock();
long long sum = 0;
for (unsigned i = 0; i < 100000; ++i)
{
// Primary loop
for (unsigned c = 0; c < arraySize; ++c)
{
if (data[c] >= 128)
sum += data[c];
}
}
double elapsedTime = static_cast<double>(clock() - start) / CLOCKS_PER_SEC;
std::cout << elapsedTime << std::endl;
std::cout << "sum = " << sum << std::endl;
}
gcc -O3
使用cmov作为条件,因此它延长了循环携带的依赖链,以包括cmov
(根据Agner Fog的指令表,这是Intel Sandybridge CPU上的2个uop和2个延迟周期。另请参阅x86标记wiki)。这是cmov
非常糟糕的情况之一。
如果数据是适度不可预测的,那么cmov
可能是一个胜利,所以对于编译器来说,这是一个相当明智的选择。(但是,编译器有时可能会过多地使用无分支代码。)
我把你的代码放在Godbolt编译器资源管理器上查看asm(突出显示并过滤掉不相关的行。不过,你仍然需要向下滚动所有排序代码才能进入main())。
.L82: # the inner loop from gcc -O3
movsx rcx, DWORD PTR [rdx] # sign-extending load of data[c]
mov rsi, rcx
add rcx, rbx # rcx = sum+data[c]
cmp esi, 127
cmovg rbx, rcx # sum = data[c]>127 ? rcx : sum
add rdx, 4 # pointer-increment
cmp r12, rdx
jne .L82
gcc本可以通过使用LEA而不是ADD来保存MOV。
由于循环的一次迭代使用CMO写入rbx,而下一次迭代则使用ADD读取rbx,因此ADD->CMOV的延迟(3个周期)受到循环瓶颈的影响。
该循环仅包含8个融合域uop,因此它可以每2个循环发出一个。执行端口压力也不像sum
dep链的延迟那样是一个严重的瓶颈,但它很接近(Sandybridge只有3个ALU端口,不像Haswell的4个)。
顺便说一句,将其写为sum += (data[c] >= 128 ? data[c] : 0);
以将cmov
从循环携带的dep链中取出可能是有用的。仍然有很多指令,但每次迭代中的cmov
是独立的。这在gcc6.3 -O2
及更早版本中按预期编译,但gcc7在关键路径上去优化为cmov
(https://gcc.gnu.org/bugzilla/show_bug.cgi?id=82666)。(它还使用比if()
更早的gcc版本进行自动矢量化。)
Clang使cmov偏离关键路径,即使使用原始源。
gcc -O2
使用了一个分支(适用于gcc5.x及更旧版本),它可以很好地预测,因为您的数据是经过排序的。由于现代CPU使用分支预测来处理控制依赖关系,因此循环携带的依赖链更短:只有add
(1个周期延迟)。
由于分支预测+推测执行,每次迭代中的比较和分支是独立的,这使得在确定分支方向之前可以继续执行。
.L83: # The inner loop from gcc -O2
movsx rcx, DWORD PTR [rdx] # load with sign-extension from int32 to int64
cmp ecx, 127
jle .L82 # conditional-jump over the next instruction
add rbp, rcx # sum+=data[c]
.L82:
add rdx, 4
cmp rbx, rdx
jne .L83
有两个循环携带的依赖链:sum
和循环计数器。sum
是0或1个周期长,并且循环计数器总是1个周期。然而,这个循环是Sandybridge上的5个融合域uop,所以它无论如何都不能以每次迭代1c的速度执行,所以延迟不是瓶颈。
它可能每2个周期运行一次迭代(分支指令吞吐量受到瓶颈限制),而-O3循环每3个循环运行一次。下一个瓶颈是ALU-uop吞吐量:4个ALU-uop(在未采用的情况下),但只有3个ALU端口。(ADD可以在任何端口上运行)。
此管道分析预测与-O3的约3秒时间与-O2的约2秒时间非常匹配。
Haswell/Skylake可以以每1.25个周期运行一次未采取情况,因为它可以在与采取分支相同的周期中执行未采取分支,并且具有4个ALU端口。(或者稍微少一点,因为在每个周期4个uop的情况下,5个uop循环不会很有问题)。
(刚刚测试:Skylake@3.9GHz在1.45s内运行整个程序的分支版本,或在1.68s内运行无分支版本。因此差异要小得多。)
g++6.3.1即使在-O2
下也使用cmov
,但g++5.4的行为仍然与4.9.2类似。
对于g++6.3.1和g++5.4,使用-fprofile-generate
/-fprofile-use
即使在-O3
(使用-fno-tree-vectorize
)时也会产生分支版本。
来自较新gcc的循环的CMOV版本使用add ecx,-128
/cmovge rbx,rdx
而不是CMP/CMOV。这有点奇怪,但可能不会减缓速度。ADD写入输出寄存器和标志,因此对物理寄存器的数量造成了更大的压力。但只要这不是一个瓶颈,它应该是平等的。
较新的gcc使用-O3自动向量化循环,即使只有SSE2,这也是一个显著的加速。(例如,我的i7-6700k Skylake运行矢量化版本在0.74s,所以大约是标量的两倍快。或0.35秒内的-O3 -march=native
,使用AVX2 256b矢量)。
矢量化版本看起来像很多指令,但也不算太糟,而且它们中的大多数都不是循环携带的dep链的一部分。它只需要在接近末尾时解压缩到64位元素。不过,它做了两次pcmpgtd
,因为它没有意识到,当条件已经将所有负整数归零时,它可以只进行零扩展,而不是符号扩展。
- C++我的数学有什么问题,为什么我的代码不能正确循环
- 代码在main()中运行,但在函数中出现错误
- 在VS代码中交叉编译Windows与Linux上的MinGW的SDL程序
- 编译包含字符串的代码时遇到问题
- 我在c++代码中生成了一个运行时#3异常
- 如何在linux终端中同时编译和运行c++代码
- 为cl.exe(Visual Studio代码)指定命令行C++版本
- 在Linux for Windows上编译C++代码时出错
- 我的字符计数代码计算错误.为什么
- 孤立代码块在结构中引发异常
- 在编译C++代码(具有dlib和opencv)到WASM时面临问题
- 为什么我的C#代码在调用回C++COM直到Task时会暂停.等待/线程.加入
- G 越野车吗?还有clang ?G 使用-O0和-O1选项编译了代码,而对于-O0和-O2,Clang
- 为什么如果使用优化(-O2、-O3),此代码的行为会有所不同
- gcc优化标志-O3使代码比-O2慢
- 使用emcc-O2编译基本代码时崩溃
- 编译器在程序集中优化代码时会做什么?即O2标志
- gcc 4.7.3使用O1,O2或O3会产生错误代码
- 为什么clang中的-O2或更大的优化会破坏这个代码
- 在比较Java和C++的速度时,我应该用-O3或-O2编译C++代码吗