Intel 平臺程式設計總結—-SIMD技術

NO IMAGE

SIMD是指單指令多資料技術,它已經成為Intel處理器的重要效能擴充套件。目前Intel處理器支援的SIMD技術包括MMX,SSE,AVX.
MMX提供了8個64bit的暫存器進行SIMD操作,SSE系列提供了128bit的8個暫存器進行SIMD指令操作。而最新的AVX指令則支援256bit的SIMD操作。
目前SIMD指令可以有四種方法進行使用分別是組合語言,C 類,編譯器Intrisincs和自動向量化。我們用下面的求一個整數陣列的和作為例子:
<n;i )
<n;i )

  1. int SumAarray(int *buf,int N)
  2. {
  3. int i,sum=0;
  4. for(i=0;i<N;i )
  5.    sum =buf[i];
  6. return sum;
  7. }

方法一:自動向量化
Intel編譯器支援自動向量化,通過選擇特定的處理器選項/Q[a]{T|P|N|W|K},上面的程式碼可以採用下來命令進行編譯:
icl /QxP/c /Fa test_vec.c
test_vec.c(4):(col.1)remark:LOOP WAS VECTORIZED.從上面的報告中可以看到第四行的迴圈自動向量化,還可以通過生成的彙編程式碼,看到使用了SIMD指令。

方法二:C 類
Intel C 編譯器和C 類庫中提供了一些新的C 類,這些類對應那些可以直接使用的SIMD指令(支援MMX,SSE,SSE2,不支援SSE3)的資料型別,使用時需要包含如下標頭檔案:

  1. #include <ivec.h>//MMX
  2. #include <fvec.h>//SSE(also
    include ivec.h)
  3. #include <dvec.h>//SSE2(also
    include fvec.h)

這些支援SIMD的向量型別採取下面的命名規則:前面用I和F分別表示是支援浮點還是整數SIMD指令,接下來是數字取值為8,16,32,64,表示組向量的基本元素大小。然後後面為字串vec,最後的陣列取值為8,4,2,1,表示組成向量的基本元素的個數。使用64bit的MMX技術的整數類包括I64vec1,I32vec2,I16vec4和I8vec8,而使用128bit的XMM暫存器的浮點類則包括F32vec4,F32vec1,F64vec2。SSE2中使用128bit的XMM暫存器,整數類包括:I128vec1,I64vec2,I32vec4,I16vec8,I8vec16,為了進一步區分封裝的是有符號整數還是無符號整數,在那些整數之後也可以包含一個符號標誌s或者u,比如Iu32vec4.
通過類的封裝,程式設計師無須關心那些對於類的運算到底使用了哪些彙編指令或者SIMD intrinsic函式,應用易於閱讀和編碼,並且沒有直接使用SIMD程式碼,在不同的處理器之間不需要任何改動,但是其缺點是無法訪問所有的指令和資料型別的組合。

下面的程式碼給出了SumAarray採用C 類的實現。

點選(此處)摺疊或開啟

  1. #include
  2. int SumAarray(int *buf,int
    N)
  3. {
  4. int i;
  5. I32vec4 *vec4 = (I32vec4 *)buf;
  6. I32vec4 sum(0,0,0,0);
  7. for(i=0;i<N;i )
  8.  sum  = vec4[i];
  9.  return sum[0] sum[1] sum[2] sum[3];
  10. }

方法三:使用intrinsics
SIMD intrinsics有些類似於C語言中的函式,可以被其它的程式碼直接呼叫。可以像其它函式呼叫一樣給它傳遞引數,Intel C 編譯器支援SIMD intrinsics,並且可以針對intrinsics函式進行內聯等優化。intrinsics能夠被直接對映到一條或者多條SIMD指令以及其它彙編指令。至於暫存器分配、指令排程和定址模式,則留給編譯器處理。因此,相比組合語言來說更容易使用。但是,對於生成指令的控制能力則更小。為了使用與SIMD技術相關的intrinsics,首先需要包含那些定義了資料型別和函式的標頭檔案。

  1. #include <mmintrin.h>//mmx
  2. #include <xmmintrin.h>//sse
  3. #include <emmintrin.h>//sse2
  4. #include <pmmintrin.h>//sse3

這些標頭檔案定義了一些資料型別對應於那些SIMD指令要使用的封裝浮點數和整數,這些資料型別名以兩個下劃線開始:
__m64用於表示一個MMX暫存器,表示封裝了8個8bit,4個16bit,2個32bit,1個64bit的整數;_
_m128用於SSE,表示封裝了;4個32bit的單精度浮點數;
_m128d可以封裝2個64bit的雙精度浮點數;
_m128i用於表示128bit SIMD整數運算的XMM暫存器。
__m128,__m128d,__m128i的記憶體變數位於16byte的邊界;

  1. int SumArray(int *buf,int N)
  2. {
  3.  int i;
  4. __m128i *vec128 = (__m128i *)buf;
  5. __m128 sum;
  6. sum = _mm_sub_epi32(sum,sum);
  7. for(i=0;i<N/4;i )
  8.    sum = _mm_add_epi32(sum,vec128[i]);
  9. sum = _mm_add_epi32(sum,_mm_srli_si128(sum,8));
  10. sum = _mm_add_epi32(sum,_mm_srli_si128(sum,4));
  11. return _mm_cvtsi128_si32(sum);
  12. }
  13. }

我們再來看一段程式碼,注意在迴圈體有一個條件語句,使用/QxP選項進行編譯會發現Intel編譯器並不會進行自動向量化。

點選(此處)摺疊或開啟

  1. #define SIZE 128
  2. __decspec(align(16)) short int aa[SIZE],bb[SIZE],cc[SIZE],dd[SIZE];
  3. void Branch_Loop(short int g)
  4. {
  5.   int i;
  6.    for(i=0;i<SIZE;i )
  7.    {
  8.      aa[i] = (bb[i]>0)?(cc[i] 2):(dd[i] g);
  9.    }
  10. }

我們可以手工來實現上面的函式,通過使用SIMD指令來消除迴圈上的分支,同時可以一次完成8個處理,減少迴圈次數。__mm_cmpgt_epi16()函式可以在一次執行時完成8個比較操作,如果其中某個組元素大於0,則返回的XMM暫存器中對應的位元位全為1,否則全為0,。獲得這些掩碼之後就可以通過下面程式碼中的三條操作完成分支賦值了。
程式碼如下:

點選(此處)摺疊或開啟

  1. #include <emmintrin.h>
  2. #define TOVectorAddress(x) ((__m128i *)&(x))
  3. void Branch_Loop(short int g)
  4. {
  5.   __m128i a,b,c,d,mask,zero,two,g_broadcast;
  6.   int i;
  7.  zero = _mm_set1_epi(16);
  8.  two = _mm_set1_epi16(2);
  9.  g_broadcast = _mm_set1_epi16(g);
  10.  for(i=0;i<SIZE;i =8)
  11.  {
  12.   b = _mm_load_si128(ToVectorAddress(bb[i]));
  13.   c = _mm_load_si128(ToVectorAddress(cc[i]));
  14.   d = _mm_load_si128(ToVectorAddress(dd[i]));
  15.   c = _mm_add_epi16(c,tw0);
  16.   d = _mm_add_epi16(d,g_broadcast);
  17.   maks = _mm_cmpgt_epi16(b,zero);
  18.  //注意下面三行程式碼,它完成了之前程式碼中的分支賦值操作,從而便於軟體流水執行
  19.   a = _mm_and_si128(c,mask);
  20.   mask = _mm_andnot_epi16(mask,d);
  21.   a = _mm_or_si128(a,mask);
  22.    _mm_store_si128(ToVectorAddress(aa[i]),a);
  23.  }
  24. }