2016-08-17 46 views
2

我是SSE編程新手,所以我希望有人能幫助我。我最近使用GCC SSE內在函數實現了一個函數來計算32位整數數組的總和。下面給出了我的實現代碼。大型陣列尺寸的SSE性能下降

int ssum(const int *d, unsigned int len) 
{ 
    static const unsigned int BLOCKSIZE=4; 
    unsigned int i,remainder; 
    int output; 
    __m128i xmm0, accumulator; 
    __m128i* src; 

    remainder = len%BLOCKSIZE; 
    src = (__m128i*)d; 
    accumulator = _mm_loadu_si128(src); 

    output = 0; 
    for(i=BLOCKSIZE;i<len-remainder;i+=BLOCKSIZE){ 
    xmm0 = _mm_loadu_si128(++src); 
    accumulator = _mm_add_epi32(accumulator,xmm0); 
    } 

    accumulator = _mm_add_epi32(accumulator, _mm_srli_si128(accumulator, 8)); 
    accumulator = _mm_add_epi32(accumulator, _mm_srli_si128(accumulator, 4)); 
    output = _mm_cvtsi128_si32(accumulator); 


    for(i=len-remainder;i<len;i++){ 
    output += d[i]; 
    } 
    return output; 
} 

正如你可以看到,這是一個相當簡單的實現方式,其中我使用擴展XMM寄存器在一個時間求和陣列4,然後通過加入餘下的元件清理在末端。

然後,我將該SIMD實現的性能與普通的for循環進行了比較。這個實驗的結果可以在這裏找到:

SIMD vs. for-loop

正如你可以看到,相較於一個for循環,這個實現確實表現出約〜60%的加速爲輸入尺寸(指的長度陣列)高達約5M元素。然而,對於輸入大小的較大值,與for循環有關的性能會急劇下降,並且只會產生大約20%的加速。

我無法解釋這種戲劇性的下降。我或多或少地通過內存線性地步進,所以緩存未命中和頁面錯誤的影響應該對於兩種實現大致相同。我在這裏錯過了什麼?有什麼辦法可以將這條曲線弄平嗎?任何想法將不勝感激。

+1

你使用的是什麼CPU? –

+2

首先,你是否檢查gcc是否會自動化標量代碼?其次,你可能會受限於內存帶寬。 – EOF

+0

正如@EOF所說,你在循環中幾乎不做任何事(一個SIMD算術指令),所以當你有大的數組時,你很可能會限制內存帶寬。 –

回答

4

對於大量輸入,數據在緩存之外,並且代碼是內存有界的。
對於較小的輸入,數據位於緩存內(即L1/L2/L3緩存),並且代碼被計算限制。
我假設你在性能測量之前沒有嘗試刷新緩存。

緩衝存儲器位於CPU內部,高速緩衝存儲器和ALU(或SSE)單元之間的帶寬非常高(高帶寬 - 少時間傳輸數據)。
您的最高級別緩存(即L3)大小約爲4MB到8MB(取決於您的CPU型號)。
數據量較大的數據必須位於DDR SDRAM上,這是外部RAM(CPU外部)。
CPU通過內存總線連接到DDR SDRAM,帶寬比緩存低得多。

示例:
假設您的外部RAM類型爲Dual Channel DDR3 SDRAM 1600。 外部RAM和CPU之間的最大理論帶寬約爲25GB /秒。

從RAM中讀取100MBytes的數據(25GB/S)大約需要100e6/25e9 = 4msec。
根據我的經驗,利用的帶寬約爲理論帶寬的一半,因此讀取時間約爲8毫秒。

計算時間更短:
假設您的循環的每次迭代需要大約2個CPU時鐘(只是一個示例)。
每次迭代處理16個字節的數據。
處理100MB的總CPU時鐘大約需要(100e6/16)* 2 = 12500000個時鐘。
假設CPU頻率是3GHz。
總SSE處理時間約爲12500000/3e9 = 4.2毫秒。

正如你所看到的,從外部RAM讀取數據需要兩倍於SSE計算時間。由於數據傳輸和計算並行發生,總時間最大爲4.2msc和8msec(即8msec)。

假設沒有使用SSE的循環會花費兩倍的計算時間,所以如果不使用SSE,計算時間約爲8.4毫秒。

在上面的例子中,使用SSE的總體改進大約是0.4毫秒。

注意:所選數字僅用於示例目的。


基準:
我做了我的系統上的一些基準。
我正在使用Windows 10和Visual Studio 2010.
基準測試:總結100MB數據(總計25 * 1024^2 32位整數)。

CPU

  • Intel酷睿i5 3550(Ivy Bridge的)。
  • CPU基頻爲3.3GHz。
  • 測試過程中的實際核心速度:3.6GHz(啓用了渦輪增壓)。
  • L1數據緩存大小:32KBytes。
  • L2高速緩存大小:256Bytes(單核心L2高速緩存大小)。
  • L3緩存大小:6MBytes。

內存:

  • 8GB DDR3雙通道。
  • RAM頻率:666MHz(相當於1333MHz無DDR)。內存理論最大帶寬:(128 * 1333/8)/ 1024 = 20.8GBytes/Sec。內存理論最大帶寬:(128 * 1333/8)/ 1024 = 20.8GBytes/Sec。

  1. 薩姆100MB如大塊與SSE(在外部RAM中的數據)。
    處理時間:6.22msec
  2. 總和1KB 100次與SSE(緩存內的數據)。
    處理時間:3.86msec
  3. 總計100MB爲大塊沒有SSE(外部RAM中的數據)。
    處理時間:8.1毫秒
  4. 總計1KB 100倍無SSE(緩存內的數據)。
    處理時間:4。73msec

可利用的存儲器帶寬:100/6.22 = 16GB /秒(分割數據大小通過時間)。 (3.6e9 * 3.86e-3)/(25/4 * 1024^2)= 2.1 clks/iteration(將總CPU時鐘數除以迭代次數)(每個迭代的平均時鐘數爲高速緩存) )

+0

這是這樣一個詳細的答案。謝謝!鑑於你獲得的結果,你認爲軟件緩衝可能有什麼優勢嗎?基本上一次將DRAM 32KB的內存複製到第二個軟件管理的緩衝區並在那裏進行計算?我會想象這會加載到主內存中的性能命中,並且不會導致高速緩存以64字節(高速緩存行的長度)丟失。我不是計算機體系結構方面的專家,所以請隨時告訴我這是否是完全瘋狂的談話。 – voidbip

+0

當我編程到DSP(從外部RAM到DSP內部RAM的初始DMA傳輸)時,我曾經這樣做。現代x86體系結構使用自動預取機制來檢測內存訪問模式(即加入地址)並在程序使用它之前將數據讀入緩存。緩存未命中並不是問題,性能受內存帶寬的限制(你不能從DDR SDRAM更快地讀取數據)。 – Rotem

+0

@voidbip:這是一個好主意,但實際上很糟糕。這意味着您的所有計算都不會與第一次觸摸冷內存時發生的緩存未命中事件重疊。 L1/L2/L3緩存可以緩存主內存的任何部分,而額外的拷貝只會增加緩存的佔用空間。反彈到L1中保持熱點的小緩衝區對於非常罕見的情況非常有用,例如[將NT視頻RAM中的NT加載與NT存儲分隔到常規RAM](https://software.intel.com/zh-cn/articles/copying -accelerated視頻解碼幀緩衝器)。直到/除非你理解那篇文章,否則不要這樣做。 –