![](/img/trans.png)
[英]g++ Optimization : O2 flag fixes a broken code where O3 breaks it again
[英]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
。 謝謝你的幫助。
PS添加代碼
#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 cmov
和2個周期的延遲。另見x86標簽維基)。 這是cmov
糟糕的情況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。
ADD-> CMOV(3個周期)延遲的循環瓶頸,因為循環的一次迭代將rbx寫入CMO,下一次迭代使用ADD讀取rbx。
該循環僅包含8個融合域uop,因此它可以每2個循環發出一次。 執行端口壓力也不像sum
鏈的延遲那樣是一個瓶頸,但它很接近(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 )。 (它還使用早於gcc版本的自動矢量化而不是if()
編寫它的方式。)
即使使用原始信號源,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個融合域uops,因此無論如何它不能以每次迭代1c執行,因此延遲不是瓶頸。
它可能每2個周期運行大約一次迭代(分支指令吞吐量瓶頸),而-O3循環每3個循環運行一次。 下一個瓶頸是ALU uop吞吐量:4個ALU uops(在未采用的情況下)但只有3個ALU端口。 (ADD可以在任何端口上運行)。
這個管道分析預測幾乎完全匹配-O3約為3秒的時間與-O2時約為2秒。
Haswell / Skylake可以每1.25個周期運行一次未采用的情況,因為它可以在與采用分支相同的周期內執行未采用的分支,並具有4個ALU端口。 (或者稍微少一點,因為5 uop循環在每個循環4個uop時不會發出相當大的問題 )。
(剛剛測試過:Skylake @ 3.9GHz在1.45秒內運行整個程序的分支版本,或在1.68秒內運行無分支版本。所以差異在那里小得多。)
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.74秒內運行矢量化版本,因此大約是標量的兩倍。或者-O3 -march=native
0.35秒的-O3 -march=native
,使用AVX2 256b矢量)。
矢量化版本看起來像很多指令,但它並不太糟糕,而且大多數都不是循環傳輸的dep鏈的一部分。 它只需要在最后解包到64位元素。 然而,它執行pcmpgtd
兩次,因為當條件已經將所有負整數歸零時,它沒有意識到它只能零擴展而不是符號擴展。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.