2011-09-01 75 views
10

我注意到有時MSVC 2010根本沒有重新排序SSE指令。因爲編譯器處理的最好,我認爲我不必關心循環內部的指令順序,這似乎並不是這種情況。SSE微優化指令訂單

我應該怎麼想?什麼決定最佳指令順序?我知道一些指令比其他指令具有更高的延遲,並且某些指令可以在CPU級別上並行/異步運行。在上下文中哪些指標是相關的?我可以在哪裏找到它們?

我知道我可以避免通過分析這個問題,但是這樣的廓線儀價格昂貴(VTune™可視化XE)和我想知道它背後的理論,而不僅僅是emperical結果。

另外我應該關心軟件預取(_mm_prefetch),或者我可以假設CPU會比我做得更好嗎?

可以說我有以下功能。我應該交錯一些指示嗎?我應該在溪流前做商店,按順序完成所有的裝載,然後進行計算,等等......?我是否需要考慮USWC與非USWC,以及時間還是非時間?

  auto cur128  = reinterpret_cast<__m128i*>(cur); 
      auto prev128 = reinterpret_cast<const __m128i*>(prev); 
      auto dest128 = reinterpret_cast<__m128i*>(dest; 
      auto end  = cur128 + count/16; 

      while(cur128 != end)    
      { 
       auto xmm0 = _mm_add_epi8(_mm_load_si128(cur128+0), _mm_load_si128(prev128+0)); 
       auto xmm1 = _mm_add_epi8(_mm_load_si128(cur128+1), _mm_load_si128(prev128+1)); 
       auto xmm2 = _mm_add_epi8(_mm_load_si128(cur128+2), _mm_load_si128(prev128+2)); 
       auto xmm3 = _mm_add_epi8(_mm_load_si128(cur128+3), _mm_load_si128(prev128+3)); 

            // dest128 is USWC memory 
       _mm_stream_si128(dest128+0, xmm0); 
       _mm_stream_si128(dest128+1, xmm1); 
       _mm_stream_si128(dest128+2, xmm2);; 
       _mm_stream_si128(dest128+3, xmm3); 

            // cur128 is temporal, and will be used next time, which is why I choose store over stream 
       _mm_store_si128 (cur128+0, xmm0);    
       _mm_store_si128 (cur128+1, xmm1);     
       _mm_store_si128 (cur128+2, xmm2);     
       _mm_store_si128 (cur128+3, xmm3); 

       cur128 += 4; 
       dest128 += 4; 
       prev128 += 4; 
      } 

      std::swap(cur, prev); 
+1

我認爲這個問題的答案必須是在測量試驗。儘管x86已經有[OOE](http://en.wikipedia.org/wiki/Out-of-order_execution)很長一段時間了,但無論排序如何,它都可以很好地處理這種情況。 – Flexo

+0

測試總是最好的。但是在這種情況下,它需要一個相當昂貴的分析器,例如, VTune XE。我想更多地瞭解它背後的理論,而不是實證結果。 OOE走多遠?這是內存延遲還是指令延遲?如果重新訂購,OOE是否照顧可以並行運行的指令? – ronag

+0

你可以發佈這個發佈版本的彙編程序輸出嗎?看看編譯器用這個做什麼會很有趣。 – Skizz

回答

9

我同意每個人都說測試和調整是最好的方法。但是有一些技巧可以幫助它。

首先,MSVC 確實重新排序SSE指令。你的例子可能太簡單或已經是最優的。

一般來說,如果你有足夠的寄存器這樣做,完全交錯往往會給出最好的結果。更進一步,請展開足夠的循環以使用所有寄存器,但不要太多以致漏出。 在你的例子中,循環完全受到內存訪問的限制,所以沒有太多空間可以做得更好。

在大多數情況下,沒有必要獲得完美的指令順序以實現最佳性能。只要「足夠接近」,編譯器或硬件的亂序執行都可以爲您解決問題。

我用它來確定,如果我的代碼是最佳的方法是關鍵路徑和瓶頸分析。在我編寫循環之後,我查找了哪些指令使用哪些資源。使用這些信息,我可以計算性能的上限,然後將其與實際結果進行比較,以查看我與最優的距離有多遠。

例如,假設我有100將與50個相乘的環路。在英特爾和AMD(推土機推土機)上,每個核心可以在每個週期支持一個SSE/AVX添加和一個SSE/AVX乘法。 由於我的循環有100個增加,我知道我不能做任何比100個週期更好的。是的,乘數在一半時間內都會閒置,但加法器是瓶頸。

現在我去和我的時間循環,我得到每個迭代週期105。這意味着我非常接近最佳狀態,並且沒有太多的收穫。但是,如果我獲得了250個週期,那麼這意味着循環出現問題,值得更多修補。

關鍵路徑分析遵循同樣的想法。查找所有指令的延遲時間並查找循環關鍵路徑的週期時間。如果你的實際表現非常接近它,那麼你已經是最佳了。

昂納霧對當前處理器的內部細節有很大的參考: http://www.agner.org/optimize/microarchitecture.pdf

6

我剛剛建立這個使用VS2010 32位編譯器,我得到以下幾點:

void F (void *cur, const void *prev, void *dest, int count) 
{ 
00901000 push  ebp 
00901001 mov   ebp,esp 
00901003 and   esp,0FFFFFFF8h 
    __m128i *cur128  = reinterpret_cast<__m128i*>(cur); 
00901006 mov   eax,220h 
0090100B jmp   F+10h (901010h) 
0090100D lea   ecx,[ecx] 
    const __m128i *prev128 = reinterpret_cast<const __m128i*>(prev); 
    __m128i *dest128 = reinterpret_cast<__m128i*>(dest); 
    __m128i *end  = cur128 + count/16; 

    while(cur128 != end)    
    { 
    auto xmm0 = _mm_add_epi8(_mm_load_si128(cur128+0), _mm_load_si128(prev128+0)); 
00901010 movdqa  xmm0,xmmword ptr [eax-220h] 
    auto xmm1 = _mm_add_epi8(_mm_load_si128(cur128+1), _mm_load_si128(prev128+1)); 
00901018 movdqa  xmm1,xmmword ptr [eax-210h] 
    auto xmm2 = _mm_add_epi8(_mm_load_si128(cur128+2), _mm_load_si128(prev128+2)); 
00901020 movdqa  xmm2,xmmword ptr [eax-200h] 
    auto xmm3 = _mm_add_epi8(_mm_load_si128(cur128+3), _mm_load_si128(prev128+3)); 
00901028 movdqa  xmm3,xmmword ptr [eax-1F0h] 
00901030 paddb  xmm0,xmmword ptr [eax-120h] 
00901038 paddb  xmm1,xmmword ptr [eax-110h] 
00901040 paddb  xmm2,xmmword ptr [eax-100h] 
00901048 paddb  xmm3,xmmword ptr [eax-0F0h] 

    // dest128 is USWC memory 
    _mm_stream_si128(dest128+0, xmm0); 
00901050 movntdq  xmmword ptr [eax-20h],xmm0 
    _mm_stream_si128(dest128+1, xmm1); 
00901055 movntdq  xmmword ptr [eax-10h],xmm1 
    _mm_stream_si128(dest128+2, xmm2);; 
0090105A movntdq  xmmword ptr [eax],xmm2 
    _mm_stream_si128(dest128+3, xmm3); 
0090105E movntdq  xmmword ptr [eax+10h],xmm3 

    // cur128 is temporal, and will be used next time, which is why I choose store over stream 
    _mm_store_si128 (cur128+0, xmm0);    
00901063 movdqa  xmmword ptr [eax-220h],xmm0 
    _mm_store_si128 (cur128+1, xmm1);     
0090106B movdqa  xmmword ptr [eax-210h],xmm1 
    _mm_store_si128 (cur128+2, xmm2);     
00901073 movdqa  xmmword ptr [eax-200h],xmm2 
    _mm_store_si128 (cur128+3, xmm3); 
0090107B movdqa  xmmword ptr [eax-1F0h],xmm3 

    cur128 += 4; 
00901083 add   eax,40h 
00901086 lea   ecx,[eax-220h] 
0090108C cmp   ecx,10h 
0090108F jne   F+10h (901010h) 
    dest128 += 4; 
    prev128 += 4; 
    } 
} 

這表明編譯器重新排序的說明,下面的一般規則「不使用寫入寄存器後立即註冊「。它也將兩個負載和一個添加到單個負載和一個從內存添加。沒有理由不能自己寫這樣的代碼,並使用所有的SIMD寄存器而不是你目前使用的四個。您可能希望將加載的字節總數與高速緩存行的大小相匹配。這將使硬件預取有機會在需要之前填充下一個緩存行。另外,預取,特別是在代碼中依次讀取存儲器,通常是不必要的。 MMU一次最多可以預取四個數據流。

1

我也想推薦的英特爾®架構代碼分析器:

https://software.intel.com/en-us/articles/intel-architecture-code-analyzer

它是一個靜態的代碼分析器,幫助找出/優化關鍵路徑,延遲和吞吐量。它適用於Windows,Linux和MacOs(我只在Linux上試過)。文檔中有一個簡單的例子,介紹如何使用它(即如何通過重新排序指令來避免延遲)。

+0

這很好,但不再維護。最後一個支持的微體系結構是Haswell。調優Skylake時,這仍然很有用,但希望英特爾會再次開始更新。這並不完美,有很多限制,偶爾它的數字不符合真正的硬件,但它絕對有用。 –