2017-02-21 36 views
8

我在一段C/C++代碼中遇到了一段非常奇怪的表現行爲,正如標題中所建議的那樣,我不知道該如何解釋。添加打印語句會使代碼速度加快一個數量級

這裏是一個爲緊密型的 - 我發現的到最小的工作示例[編輯:請參閱下面的一個較短]:

#include <stdio.h> 
#include <stdlib.h> 
#include <complex> 

using namespace std; 

const int pp = 29; 
typedef complex<double> cdbl; 

int main() { 
    cdbl ff[pp], gg[pp]; 
    for(int ii = 0; ii < pp; ii++) { 
    ff[ii] = gg[ii] = 1.0; 
    } 

    for(int it = 0; it < 1000; it++) { 
    cdbl dual[pp]; 

    for(int ii = 0; ii < pp; ii++) { 
     dual[ii] = 0.0; 
    } 

    for(int h1 = 0; h1 < pp; h1 ++) { 
     for(int h2 = 0; h2 < pp; h2 ++) { 
     cdbl avg_right = 0.0; 
     for(int xx = 0; xx < pp; xx ++) { 
      int c00 = xx, c01 = (xx + h1) % pp, c10 = (xx + h2) % pp, 
       c11 = (xx + h1 + h2) % pp; 
      avg_right += ff[c00] * conj(ff[c01]) * conj(ff[c10]) * gg[c11]; 
     } 
     avg_right /= static_cast<cdbl>(pp); 

     for(int xx = 0; xx < pp; xx ++) { 
      int c01 = (xx + h1) % pp, c10 = (xx + h2) % pp, 
       c11 = (xx + h1 + h2) % pp; 
      dual[xx] += conj(ff[c01]) * conj(ff[c10]) * ff[c11] * conj(avg_right); 
     } 
     } 
    } 
    for(int ii = 0; ii < pp; ii++) { 
     dual[ii] = conj(dual[ii])/static_cast<double>(pp*pp); 
    } 

    for(int ii = 0; ii < pp; ii++) { 
     gg[ii] = dual[ii]; 
    } 

#ifdef I_WANT_THIS_TO_RUN_REALLY_FAST 
    printf("%.15lf\n", gg[0].real()); 
#else // I_WANT_THIS_TO_RUN_REALLY_SLOWLY 
#endif 

    } 
    printf("%.15lf\n", gg[0].real()); 

    return 0; 
} 

這裏是運行這個結果我係統:

[email protected] $ g++ -o test.elf test.cc -Wall -Wextra -O2 
[email protected] $ time ./test.elf > /dev/null 
    real 0m7.329s 
    user 0m7.328s 
    sys  0m0.000s 
[email protected] $ g++ -o test.elf test.cc -Wall -Wextra -O2 -DI_WANT_THIS_TO_RUN_REALLY_FAST 
[email protected] $ time ./test.elf > /dev/null 
    real 0m0.492s 
    user 0m0.490s 
    sys  0m0.001s 
[email protected] $ g++ --version 
g++ (Gentoo 4.9.4 p1.0, pie-0.6.4) 4.9.4 [snip] 

這不是什麼代碼是計算非常重要:它只是一個在長度29的陣列複雜運算的每噸它已經「簡化」從複雜的算術大得多噸,我所關心的。

所以,行爲似乎是,如標題所述:如果我把這個打印語句放回去,代碼會快得多。

我玩過一段時間:例如,打印常量字符串不會提高加速度,但打印時鐘的時間確實如此。有一個非常明確的門檻:代碼是快或慢。

我考慮過一些奇怪的編譯器優化可能會啓動或不啓動的可能性,也許取決於代碼是否有副作用。但是,如果是這樣,它非常微妙:當我查看反彙編的二進制文件時,它們看起來是相同的,只是它有一個額外的打印語句,它們使用不同的可互換寄存器。我可能(必須)錯過了一些重要的東西。

我完全喪失解釋什麼是地球可能造成這種情況。更糟糕的是,它的確影響了我的生活,因爲我正在運行相關的代碼,並且插入額外的打印語句並不是一個好的解決方案。

任何可能的理論都會受到歡迎。如果你可以解釋如何解釋任何事情,那麼可以接受「你的電腦壞了」的回答。


UPDATE:有道歉的問題越來越長,我已經縮水的例子

#include <stdio.h> 
#include <stdlib.h> 
#include <complex> 

using namespace std; 

const int pp = 29; 
typedef complex<double> cdbl; 

int main() { 
    cdbl ff[pp]; 
    cdbl blah = 0.0; 
    for(int ii = 0; ii < pp; ii++) { 
    ff[ii] = 1.0; 
    } 

    for(int it = 0; it < 1000; it++) { 
    cdbl xx = 0.0; 

    for(int kk = 0; kk < 100; kk++) { 
     for(int ii = 0; ii < pp; ii++) { 
     for(int jj = 0; jj < pp; jj++) { 
      xx += conj(ff[ii]) * conj(ff[jj]) * ff[ii]; 
     } 
     } 
    } 
    blah += xx; 

    printf("%.15lf\n", blah.real()); 
    } 
    printf("%.15lf\n", blah.real()); 

    return 0; 
} 

我可以使它更小,但已經機器碼是可控的。如果我將與第一個printf的callq指令對應的二進制文件的第二個字節更改爲0x90,則執行從快速變爲慢。

對__muldc3()進行函數調用的編譯代碼非常繁重。我認爲這必須與Broadwell體系結構如何處理或不處理這些跳躍有關:兩個版本的指令數相同,因此它們在指令/週期中有所不同(約0.16比2.8)。

此外,編譯-static使事情再次變得更快。


而且無恥更新:我自覺我是唯一一個誰可以玩這個,所以這裏有一些更多的觀測:

好像調用任何庫函數—包括一些愚蠢的我編造的什麼也沒做—爲第一次,把執行進入緩慢狀態。隨後對printf,fprintf或sprintf的調用以某種方式清除狀態,並且執行速度又很快。所以,重要的是,第一次調用__muldc3()時,我們進入緩慢狀態,下一個{,f,s} printf重置所有內容。

一旦一個庫函數被調用一次,並且狀態已被重置,該函數就變成了空閒狀態,並且可以在不改變狀態的情況下儘可能多地使用它。

因此,例如:

#include <stdio.h> 
#include <stdlib.h> 
#include <complex> 

using namespace std; 

int main() { 
    complex<double> foo = 0.0; 
    foo += foo * foo; // 1 
    char str[10]; 
    sprintf(str, "%c\n", 'c'); 
    //fflush(stdout); // 2 

    for(int it = 0; it < 100000000; it++) { 
    foo += foo * foo; 
    } 

    return (foo.real() > 10.0); 
} 

是快,但註釋掉線1或取消註釋第2行使得它再次放緩。

第一次運行庫調用時,PLT中的「蹦牀」被初始化爲指向共享庫必須相關。所以,也許不知何故,這個動態加載代碼將處理器前端留在不好的地方,直到它被「救出」。

+0

如果您在不使用printf語句的情況下調用'gg [0] .real()',那麼您是否會看到相同的行爲(以這種方式它沒有被優化掉)。我嘗試了你的代碼,但是在我的系統中沒有看到相同的行爲(兩個版本都是同一時間),但是這是虛擬機,因此可能會有不同的表現。 –

+0

無法重現:https://ideone.com/DuAYHz,https://ideone.com/BY4GIq – 2501

+0

我無法重現這一點,兩者在我的系統(gcc6,64位Linux)上幾乎同樣快。你能提供關於你的設置的更多信息嗎? –

回答

2

爲了記錄,我終於明白了這一點。

事實證明,這是與AVX – SSE過渡罰款。引述this exposition from Intel

當使用英特爾®AVX指令,它知道,混合256位英特爾®AVX指令與傳統(非VEX編碼)英特爾®SSE指令可能導致可能會影響到處罰是非常重要的性能。 256位英特爾®AVX指令在256位YMM寄存器上運行,這些寄存器是現有128位XMM寄存器的256位擴展。 128位英特爾®AVX指令對YMM寄存器的低128位進行操作,將高128位置零。但是,傳統的英特爾®SSE指令在XMM寄存器上運行,並且不知道YMM寄存器的高128位。因此,從256位英特爾®AVX轉換爲傳統英特爾®SSE時,硬件會保存YMM寄存器的高128位內容,然後在從英特爾®SSE轉換回英特爾®AVX時恢復這些值( 256位或128位)。保存和恢復操作都會對每個操作造成數十個時鐘週期的損失。

我的主迴路的編譯版本以上內容包括傳統SSE指令(movapd和朋友,我認爲),而中__muldc3在libgcc_s實現使用了很多花哨的AVX指令(vmovapdvmulsd等)。

這是放緩的最終原因。 事實上,英特爾性能診斷結果表明,該AVX/SSE轉換髮生的每`__muldc3' 的通話每路幾乎一模一樣一次(在上面貼的代碼的最後一個版本):

$ perf stat -e cpu/event=0xc1,umask=0x08/ -e cpu/event=0xc1,umask=0x10/ ./slow.elf 

Performance counter stats for './slow.elf': 
    100,000,064  cpu/event=0xc1,umask=0x08/ 
    100,000,118  cpu/event=0xc1,umask=0x10/ 

(事件代碼從表中獲得19.5 of another Intel manual)。

第一次調用庫函數時會出現放緩的原因,當您調用printfsprintf或其他函數時會再次關閉。線索是in the first document again

當它是不能去除的過渡,這是經常可以通過顯式回零上128位,以避免處罰YMM寄存器,在這種情況下,硬件不保存這些值。

我想全文如下。當您第一次調用庫函數時,設置PLT的ld-linux-x86-64.so中的蹦牀代碼會使MMY寄存器的高位保持非零狀態。當您撥打sprintf以及其他內容時,會將MMY寄存器的高位清零(無論是偶然還是設計,我都不確定)。

sprintf呼叫替換爲asm("vzeroupper") —指示處理器明確地將這些高位清零—具有相同的效果。

通過將-mavx-march=native添加到編譯標誌中可以消除這種影響,這是系統其餘部分的構建方式。爲什麼這種情況在默認情況下不會發生,這只是我猜想的系統的一個祕密。

我不太清楚我們在這裏學到了什麼,但它在那裏。

相關問題