簡體   English   中英

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 謝謝你的幫助。

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個周期的延遲。另見標簽維基)。 這是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在關鍵路徑上取消優化為cmovhttps://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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM