2016-12-23 56 views
22

我一直在試圖找出應用程序中的性能問題,並最終將其縮小爲一個非常奇怪的問題。如果VZEROUPPER指令被註釋掉,以下代碼片段在Skylake CPU(i5-6500)上運行速度會減慢6倍。我測試過Sandy Bridge和Ivy Bridge CPU,兩個版本都以相同的速度運行,不管有沒有VZEROUPPER爲什麼這個SSE代碼在Skylake上沒有VZEROUPPER慢6倍?

現在我對VZEROUPPER做的事情有了一個相當好的想法,我認爲在沒有VEX編碼指令並且沒有調用任何可能包含它們的函數的情況下,對代碼應該沒有任何影響。它不支持其他支持AVX的CPU似乎支持這一點。那麼表11-2中的Intel® 64 and IA-32 Architectures Optimization Reference Manual

那麼是怎麼回事?

我唯一留下的理論是,CPU中存在一個錯誤,它不正確地觸發了「不保存AVX寄存器的上半部分」過程。或別的東西一樣奇怪。

這是main.cpp中:

#include <immintrin.h> 

int slow_function(double i_a, double i_b, double i_c); 

int main() 
{ 
    /* DAZ and FTZ, does not change anything here. */ 
    _mm_setcsr(_mm_getcsr() | 0x8040); 

    /* This instruction fixes performance. */ 
    __asm__ __volatile__ ("vzeroupper" : : :); 

    int r = 0; 
    for(unsigned j = 0; j < 100000000; ++j) 
    { 
     r |= slow_function( 
       0.84445079384884236262, 
       -6.1000481519580951328, 
       5.0302160279288017364); 
    } 
    return r; 
} 

,這是slow_function.cpp:

#include <immintrin.h> 

int slow_function(double i_a, double i_b, double i_c) 
{ 
    __m128d sign_bit = _mm_set_sd(-0.0); 
    __m128d q_a = _mm_set_sd(i_a); 
    __m128d q_b = _mm_set_sd(i_b); 
    __m128d q_c = _mm_set_sd(i_c); 

    int vmask; 
    const __m128d zero = _mm_setzero_pd(); 

    __m128d q_abc = _mm_add_sd(_mm_add_sd(q_a, q_b), q_c); 

    if(_mm_comigt_sd(q_c, zero) && _mm_comigt_sd(q_abc, zero) ) 
    { 
     return 7; 
    } 

    __m128d discr = _mm_sub_sd(
     _mm_mul_sd(q_b, q_b), 
     _mm_mul_sd(_mm_mul_sd(q_a, q_c), _mm_set_sd(4.0))); 

    __m128d sqrt_discr = _mm_sqrt_sd(discr, discr); 
    __m128d q = sqrt_discr; 
    __m128d v = _mm_div_pd(
     _mm_shuffle_pd(q, q_c, _MM_SHUFFLE2(0, 0)), 
     _mm_shuffle_pd(q_a, q, _MM_SHUFFLE2(0, 0))); 
    vmask = _mm_movemask_pd(
     _mm_and_pd(
      _mm_cmplt_pd(zero, v), 
      _mm_cmple_pd(v, _mm_set1_pd(1.0)))); 

    return vmask + 1; 
} 

功能編譯成這個與鐺:

0: f3 0f 7e e2    movq %xmm2,%xmm4 
4: 66 0f 57 db    xorpd %xmm3,%xmm3 
8: 66 0f 2f e3    comisd %xmm3,%xmm4 
c: 76 17     jbe 25 <_Z13slow_functionddd+0x25> 
e: 66 0f 28 e9    movapd %xmm1,%xmm5 
12: f2 0f 58 e8    addsd %xmm0,%xmm5 
16: f2 0f 58 ea    addsd %xmm2,%xmm5 
1a: 66 0f 2f eb    comisd %xmm3,%xmm5 
1e: b8 07 00 00 00   mov $0x7,%eax 
23: 77 48     ja  6d <_Z13slow_functionddd+0x6d> 
25: f2 0f 59 c9    mulsd %xmm1,%xmm1 
29: 66 0f 28 e8    movapd %xmm0,%xmm5 
2d: f2 0f 59 2d 00 00 00 mulsd 0x0(%rip),%xmm5  # 35 <_Z13slow_functionddd+0x35> 
34: 00 
35: f2 0f 59 ea    mulsd %xmm2,%xmm5 
39: f2 0f 58 e9    addsd %xmm1,%xmm5 
3d: f3 0f 7e cd    movq %xmm5,%xmm1 
41: f2 0f 51 c9    sqrtsd %xmm1,%xmm1 
45: f3 0f 7e c9    movq %xmm1,%xmm1 
49: 66 0f 14 c1    unpcklpd %xmm1,%xmm0 
4d: 66 0f 14 cc    unpcklpd %xmm4,%xmm1 
51: 66 0f 5e c8    divpd %xmm0,%xmm1 
55: 66 0f c2 d9 01   cmpltpd %xmm1,%xmm3 
5a: 66 0f c2 0d 00 00 00 cmplepd 0x0(%rip),%xmm1  # 63 <_Z13slow_functionddd+0x63> 
61: 00 02 
63: 66 0f 54 cb    andpd %xmm3,%xmm1 
67: 66 0f 50 c1    movmskpd %xmm1,%eax 
6b: ff c0     inc %eax 
6d: c3      retq 

所生成的代碼與gcc不同,但它顯示了同樣的問題。舊版本的intel編譯器會產生功能的另一個變體,它也會顯示問題,但前提是main.cpp不是由intel編譯器構建的,因爲它會插入調用來初始化某些自己的庫,這些庫可能最終會在某處執行VZEROUPPER

當然,如果整個東西都是用AVX支持構建的,所以內在函數變成了VEX編碼指令,也沒有問題。

我已經試過用linux在perf上分析代碼,大部分運行時通常在1-2條指令上,但並不總是相同的,這取決於我的配置文件(gcc,clang,intel)的哪個版本。縮短功能看起來會使性能差異逐漸消失,因此看起來好像有幾條指令導致了這個問題。

編輯:這是一個純的程序集版本,爲Linux。下面的評論。

.text 
    .p2align 4, 0x90 
    .globl _start 
_start: 

    #vmovaps %ymm0, %ymm1 # This makes SSE code crawl. 
    #vzeroupper   # This makes it fast again. 

    movl $100000000, %ebp 
    .p2align 4, 0x90 
.LBB0_1: 
    xorpd %xmm0, %xmm0 
    xorpd %xmm1, %xmm1 
    xorpd %xmm2, %xmm2 

    movq %xmm2, %xmm4 
    xorpd %xmm3, %xmm3 
    movapd %xmm1, %xmm5 
    addsd %xmm0, %xmm5 
    addsd %xmm2, %xmm5 
    mulsd %xmm1, %xmm1 
    movapd %xmm0, %xmm5 
    mulsd %xmm2, %xmm5 
    addsd %xmm1, %xmm5 
    movq %xmm5, %xmm1 
    sqrtsd %xmm1, %xmm1 
    movq %xmm1, %xmm1 
    unpcklpd %xmm1, %xmm0 
    unpcklpd %xmm4, %xmm1 

    decl %ebp 
    jne .LBB0_1 

    mov $0x1, %eax 
    int $0x80 

好的,正如在評論中懷疑的那樣,使用VEX編碼指令會導致放緩。使用VZEROUPPER清除它。但這仍然無法解釋原因。

據我所知,不使用VZEROUPPER應該涉及過渡到舊的SSE指令的成本,但不是永久減速。尤其不是那麼大。考慮到循環開銷,這個比率至少是10倍,或許更多。

我已經嘗試搞亂組裝一點點和浮動指令一樣糟糕的雙重。我無法將問題指向單個指令。

+1

你使用了哪些編譯器標誌?也許(隱藏)進程初始化使用了一些VEX指令,它將您置於混合狀態,從此您永遠不會退出。您可以嘗試複製/粘貼程序集並將其構建爲帶'_start'的純裝配程序,這樣可以避免任何編譯器插入的init代碼,並查看它是否表現出相同的問題。 – BeeOnRope

+0

@BeeOnRope我使用'-O3 -ffast-math',但即使使用'-O0',效果也是存在的。我會嘗試純粹的組裝。你可能會在[Agner的博客](http://agner.org/optimize/blog/read.php?i=415)上發現的內容,發現VEX轉換的方式有很大的內部變化處理...將需要看看。 – Olivier

+0

是的 - 但奇怪的是,在Skylake上,對於運行在「壞」混合模式下的處罰應該大大減少 - 但我沒有重新閱讀它,以便更新我對細節的記憶。 – BeeOnRope

回答

23

即使您的整個可見應用程序沒有明顯使用任何AVX指令,您仍然遇到了「混合」非VEX SSE和VEX編碼指令 - 的懲罰!。在Skylake之前,當從使用vex的代碼切換到沒有使用vex的代碼時,這種類型的懲罰只是一次性懲罰轉換懲罰,反之亦然。也就是說,除非您積極地混合VEX和非VEX,否則您從未支付過去發生的任何損失。然而,在Skylake中,即使沒有進一步的混合,也存在一種狀態,即非VEX SSE指令支付高持續執行罰金。從馬的嘴

直,這裏的圖11-1 - 老(前SKYLAKE微架構)轉換圖:

Pre-Skylake Transition Penalties

正如你可以看到,所有的處罰(紅色箭頭),將您帶入一個新的狀態,此時不再有重複該行爲的懲罰。例如,如果您通過執行一些256位AVX來達到狀態,則您執行舊版SSE,您將支付一次性懲罰以轉換到保留的非INIT上層狀態,但在此之後你不需要支付任何處罰。

在SKYLAKE微架構,一切都是每圖11-2不同:

Skylake Penalties

有較少的罰款整體,但挑剔的你的情況,其中之一是一個自我循環:點球執行舊版SSE(圖11-2中的罰款A)指令骯髒的上部狀態會使您處於該狀態。這就是發生在你身上的事情 - 任何AVX指令都會將你置於髒的高位狀態,這會降低所有進一步的SSE執行速度。

下面是英特爾稱,有關新的處罰(第11.3部分):

的SKYLAKE微架構的微架構實現不同的狀態機 比上一代管理與混合SSE和AVX指令相關 的YMM狀態轉變。在「已修改 和未保存」狀態下執行SSE指令時,它不再保存整個 上部YMM狀態,但保存單個寄存器的高位。 因此,混合SSE和AVX指令將遇到與目標寄存器的目標寄存器 的部分寄存器依賴性相關的懲罰 以及對目標寄存器的高位 的額外混合操作。

所以罰款顯然是相當大的 - 它融入頂部位全部時間來保護它們,這也使得這顯然獨立成爲相關的指令,因爲是在隱藏高位的依賴。例如,xorpd xmm0, xmm0不再破壞對先前值xmm0的依賴性,因爲結果實際上取決於ymm0的隱藏高位,這些高位不會被xorpd清除。後一種效果可能會殺死你的表現,因爲你現在有很長的依賴鏈,而這些依賴鏈從平常的分析中是不能期待的。

這是最糟糕的性能陷阱類型之一:現有體系結構的行爲/最佳實踐與當前體系結構基本相反。據推測,硬件架構師有充分的理由進行這項改變,但這只是在微妙的性能問題中增加了另一個「難題」。

我會針對插入AVX指令的編譯器或運行庫提交一個錯誤,但沒有跟進VZEROUPPER

更新:每低於OP的comment,違規(AVX)的代碼插入運行時鏈接ldbug已經存在。


從英特爾的optimization manual

+0

太棒了!首先閱讀手冊的舊版本,但沒有Skylake評論,然後新版本不夠遠,我感到困惑。不能幫助新版本的頁面數量比舊版本少。我一定會追查這個罪魁禍首。 – Olivier

+5

有問題的代碼位於_dl_runtime_resolve_avx(),/lib64/ld-linux-x86-64.so.2。似乎這應該與下一個版本的glibc相媲美:https://sourceware.org/bugzilla/show_bug.cgi?id=20495 – Olivier

+4

有趣的是,VZEROUPPER不推薦用於KNL,但情況正在討論中https:// software.intel.com/en-us/forums/intel-isa-extensions/topic/704023 –

11

我剛做了一些實驗(在Haswell上)。乾淨狀態和髒狀態之間的轉換並不昂貴,但髒狀態使得每個非VEX向量操作都依賴於目標寄存器的先前值。在你的情況下,例如movapd %xmm1, %xmm5將錯誤地依賴於ymm5,這可以防止亂序執行。這解釋了AVX代碼之後爲什麼需要vzeroupper。

+3

你是本網站[x86]標籤的英雄之一。標籤的Avid追隨者在這裏廣泛引用您的意見,因爲您是x86處理器微架構細節上罕見的資源之一。繼續你出色的工作! –

+0

很酷,但上面描述的新行爲(隱藏寄存器依賴關係的第二個圖)顯然只適用於Skylake和更新的?在Haswell,它應該將上半部分保存在某處,以便後續的非VEX操作很快。 – BeeOnRope

+0

我目前無法測試Skylake。 –