1. 程式人生 > >跨平臺SSE、AVX指令測試

跨平臺SSE、AVX指令測試

本文面對對SSE等SIMD指令集有一定基礎的讀者,以單精度浮點陣列求和為例演示瞭如何跨平臺使用SSE、AVX指令集。因使用了stdint、zintrin、ccpuid這三個模組,可以完全避免手工編寫彙編程式碼,具有很高可移植性。支援vc、gcc編譯器,在Windows、Linux、Mac這三大平臺上成功執行。


一、問題背景

  最初,我們只能使用匯編語言來編寫SIMD程式碼。不僅寫起來很麻煩,而且易讀性、可維護性、移植性都較差。
  不久,VC、GCC等編譯器相繼支援了Intrinsic函式,使我們可以擺脫彙編,利用C語言來呼叫SIMD指令集,大大提高了易讀性和可維護。而且移植性也有提高,能在同一編譯器上實現32位與64位的平滑過渡。
  但當代碼在另一種編譯器編譯時,會遇到一些問題而無法編譯。甚至在使用同一種編譯器的不同版本時,也會遇到無法編譯問題。

  首先是整數型別問題——
  傳統C語言的short、int、long等整數型別是與平臺相關的,不同平臺上的位長是不同的(例如Windows是LLP64模型,Linux、Mac等Unix系統多采用LP64模型)。而使用SSE等SIMD指令集時需要精確計算資料的位數,不同位長的資料必須使用不同的指令來處理。
  有一個解決辦法,就是使用C99標準中stdint.h所提供的指定位長的整數型別。GCC對C99標準支援性較好,而VC的步驟很慢,貌似直到VC2010才支援stdint.h。而很多時候我們為了相容舊程式碼,不得不使用VC等老版本的VC編譯器。

  其次是Intrinsic函式的標頭檔案問題,不同編譯器所使用的標頭檔案不同——
  對於早期版本VC,需要根據具體的指令集需求,手動引入mmintrin.h、xmmintrin.h等標頭檔案。對於VC2005或更高版本,引入intrin.h就行了,它會自動引入當前編譯器所支援的所有Intrinsic標頭檔案。
  對於早期版本GCC,也是手動引入mmintrin.h、xmmintrin.h等標頭檔案。而對於高版本的GCC,引入x86intrin.h就行了,它會自動引入當前編譯環境所允許的Intrinsic標頭檔案。

  再次是當前編譯環境下的Intrinsic函式集支援性問題——
  對於VC來說,VC6支援MMX、3DNow!、SSE、SSE2,然後更高版本的VC支援更多的指令集。但是,VC沒有提供檢測Intrinsic函式集支援性的辦法。例如你在VC2010上編寫了一段使用了AVX Intrinsic函式的程式碼,但拿到VC2005上就不能通過編譯了。其次,VC不支援64位下的MMX,這讓一些老程式遷徙到64位版時遭來了一些麻煩。
  而對於GCC來說,它使用-mmmx、-msse等編譯器開關來啟用各種指令集,同時定義了對應的 __MMX__、__SSE__等巨集,然後x86intrin.h會根據這些巨集來宣告相應的Intrinsic函式集。__MMX__、__SSE__等巨集可以幫助我們判斷Intrinsic函式集是否支援,但這只是GCC的專用功能。
  此外還有一些細節問題,例如某些Intrinsic函式僅在64下才能使用、有些老版本編譯器的標頭檔案缺少某個Intrinsic函式。所以我們希望有一種統一的方式來判斷Intrinsic函式集的支援性。

  除了編譯期間的問題外,還有執行期間的問題——
  在執行時,怎麼檢測當前處理器支援哪些指令集?
  雖然X86體系提供了用來檢測處理器的CPUID指令,但它沒有規範的Intrinsic函式,在不同的編譯器上的用法不同。
  而且X86體系有很多種指令集,每種指令集具體的檢測方法是略有區別的。尤其是SSE、AVX這樣的SIMD指令集是需要作業系統配合才能正常使用的,所以在CPUID檢查通過後,還需要進一步驗證。


二、範例講解

2.1 事先準備

  這三個模組的純C版就是一個頭檔案,用起來很方便,將它們專案中,直接#include就行了。例如——

  1. #define __STDC_LIMIT_MACROS 1   // C99整數範圍常量. [純C程式可以不用, 而C++程式必須定義該巨集.]
  2. #include "zintrin.h"
  3. #include "ccpuid.h"
  1. #define __STDC_LIMIT_MACROS 1   // C99整數範圍常量. [純C程式可以不用, 而C++程式必須定義該巨集.]
  2. #include "zintrin.h"
  3. #include "ccpuid.h"

  因為stdint.h會被zintrin.h或ccpuid.h引用,所以不需要手動引入它。
  因為它們用到了C99整數範圍常量,所以應該在程式的最前面定義__STDC_LIMIT_MACROS巨集(或者可以在專案配置、編譯器命令列等位置進行配置)。根據C99規範,純C程式可以不用, 而C++程式必須定義該巨集。本文為了演示,定義了該巨集。


2.2 C語言版

  我們先用C語言編寫一個基本的單精度浮點陣列求和函式——

  1. // 單精度浮點陣列求和_基本版.
  2. //
  3. // result: 返回陣列求和結果.
  4. // pbuf: 陣列的首地址.
  5. // cntbuf: 陣列長度.
  6. float sumfloat_base(constfloat* pbuf, size_t cntbuf) 
  7.     float s = 0;    // 求和變數.
  8.     size_t i; 
  9.     for(i=0; i<cntbuf; ++i) 
  10.     { 
  11.         s += pbuf[i]; 
  12.     } 
  13.     return s; 
  1. // 單精度浮點陣列求和_基本版.
  2. //
  3. // result: 返回陣列求和結果.
  4. // pbuf: 陣列的首地址.
  5. // cntbuf: 陣列長度.
  6. float sumfloat_base(constfloat* pbuf, size_t cntbuf)  
  7. {  
  8.     float s = 0;    // 求和變數.
  9.     size_t i;  
  10.     for(i=0; i<cntbuf; ++i)  
  11.     {  
  12.         s += pbuf[i];  
  13.     }  
  14.     return s;  
  15. }  

  該函式很容易理解——先將返回值賦初值0,然後迴圈加上陣列中每一項的值。


2.3 SSE版

2.3.1 SSE普通版

  SSE暫存器是128位的,對應__m128型別,它能一次能處理4個單精度浮點數。
  很多SSE指令要求記憶體地址按16位元組對齊。本文為了簡化,假定浮點陣列的首地址是總是16位元組對齊的,僅需要考慮陣列長度不是4的整數倍問題。
  因使用了SSE Intrinsic函式,我們可以根據zintrin.h所提供的INTRIN_SSE巨集進行條件編譯。
  程式碼如下——

  1. #ifdef INTRIN_SSE
  2. // 單精度浮點陣列求和_SSE版.
  3. float sumfloat_sse(constfloat* pbuf, size_t cntbuf) 
  4.     float s = 0;    // 求和變數.
  5.     size_t i; 
  6.     size_t nBlockWidth = 4;// 塊寬. SSE暫存器能一次處理4個float.
  7.     size_t cntBlock = cntbuf / nBlockWidth;// 塊數.
  8.     size_t cntRem = cntbuf % nBlockWidth;  // 剩餘數量.
  9.     __m128 xfsSum = _mm_setzero_ps();   // 求和變數。[SSE] 賦初值0
  10.     __m128 xfsLoad; // 載入.
  11.     constfloat* p = pbuf; // SSE批量處理時所用的指標.
  12.     constfloat* q;// 將SSE變數上的多個數值合併時所用指標.
  13.     // SSE批量處理.
  14.     for(i=0; i<cntBlock; ++i) 
  15.     { 
  16.         xfsLoad = _mm_load_ps(p);   // [SSE] 載入
  17.         xfsSum = _mm_add_ps(xfsSum, xfsLoad);   // [SSE] 單精浮點緊縮加法
  18.         p += nBlockWidth; 
  19.     } 
  20.     // 合併.
  21.     q = (constfloat*)&xfsSum; 
  22.     s = q[0] + q[1] + q[2] + q[3]; 
  23.     // 處理剩下的.
  24.     for(i=0; i<cntRem; ++i) 
  25.     { 
  26.         s += p[i]; 
  27.     } 
  28.     return s; 
  29. #endif  // #ifdef INTRIN_SSE
  1. #ifdef INTRIN_SSE
  2. // 單精度浮點陣列求和_SSE版.
  3. float sumfloat_sse(constfloat* pbuf, size_t cntbuf)  
  4. {  
  5.     float s = 0;    // 求和變數.
  6.     size_t i;  
  7.     size_t nBlockWidth = 4; // 塊寬. SSE暫存器能一次處理4個float.
  8.     size_t cntBlock = cntbuf / nBlockWidth; // 塊數.
  9.     size_t cntRem = cntbuf % nBlockWidth;   // 剩餘數量.
  10.     __m128 xfsSum = _mm_setzero_ps();   // 求和變數。[SSE] 賦初值0
  11.     __m128 xfsLoad; // 載入.
  12.     constfloat* p = pbuf;  // SSE批量處理時所用的指標.
  13.     constfloat* q; // 將SSE變數上的多個數值合併時所用指標.
  14.     // SSE批量處理.
  15.     for(i=0; i<cntBlock; ++i)  
  16.     {  
  17.         xfsLoad = _mm_load_ps(p);   // [SSE] 載入
  18.         xfsSum = _mm_add_ps(xfsSum, xfsLoad);   // [SSE] 單精浮點緊縮加法
  19.         p += nBlockWidth;  
  20.     }  
  21.     // 合併.
  22.     q = (constfloat*)&xfsSum;  
  23.     s = q[0] + q[1] + q[2] + q[3];  
  24.     // 處理剩下的.
  25.     for(i=0; i<cntRem; ++i)  
  26.     {  
  27.         s += p[i];  
  28.     }  
  29.     return s;  
  30. }  
  31. #endif  // #ifdef INTRIN_SSE

  上述程式碼大致可分為四個部分——
1. 變數定義與初始化。
2. SSE批量處理。即對前面能湊成4個一組的資料,利用SSE的128位寬度同時對4個數累加。
3. 合併。將__m128上的多個數值合併到求和變數。因考慮某些編譯器不能直接使用“.”來訪問__m128變數中的資料,於是利用指標q來訪問xfsSum中的資料。
4. 處理剩下的。即對尾部不能湊成4個一組的資料,採用基本的逐項相加演算法

  上述程式碼總共用到了3個SSE Intrinsic函式——
_mm_setzero_ps:對應XORPS指令。將__m128上的每一個單精度浮點數均賦0值,虛擬碼:for(i=0;i<4;++i) C[i]=0.0f。
_mm_load_ps:對應MOVPS指令。從記憶體中對齊載入4個單精度浮點數到__m128變數,虛擬碼:for(i=0;i<4;++i) C[i]=_A[i]。
_mm_add_ps:對應ADDPS指令。相加,即對2個__m128變數的4個單精度浮點數進行垂直相加,虛擬碼:for(i=0;i<4;++i) C[i]=A[i]+B[i]。


2.3.2 SSE四路迴圈展開版

  迴圈展開可以降低迴圈開銷,提高指令級並行效能。
  一般來說,四路迴圈展開就差不多夠了。我們可以很方便的將上一節的程式碼改造為四路迴圈展開版——

  1. // 單精度浮點陣列求和_SSE四路迴圈展開版.
  2. float sumfloat_sse_4loop(constfloat* pbuf, size_t cntbuf) 
  3.     float s = 0;    // 返回值.
  4.     size_t i; 
  5.     size_t nBlockWidth = 4*4;   // 塊寬. SSE暫存器能一次處理4個float,然後迴圈展開4次.
  6.     size_t cntBlock = cntbuf / nBlockWidth;// 塊數.
  7.     size_t cntRem = cntbuf % nBlockWidth;  // 剩餘數量.
  8.     __m128 xfsSum = _mm_setzero_ps();   // 求和變數。[SSE] 賦初值0
  9.     __m128 xfsSum1 = _mm_setzero_ps(); 
  10.     __m128 xfsSum2 = _mm_setzero_ps(); 
  11.     __m128 xfsSum3 = _mm_setzero_ps(); 
  12.     __m128 xfsLoad; // 載入.
  13.     __m128 xfsLoad1; 
  14.     __m128 xfsLoad2; 
  15.     __m128 xfsLoad3; 
  16.     constfloat* p = pbuf; // SSE批量處理時所用的指標.
  17.     constfloat* q;// 將SSE變數上的多個數值合併時所用指標.
  18.     // SSE批量處理.
  19.     for(i=0; i<cntBlock; ++i) 
  20.     { 
  21.         xfsLoad = _mm_load_ps(p);   // [SSE] 載入.
  22.         xfsLoad1 = _mm_load_ps(p+4); 
  23.         xfsLoad2 = _mm_load_ps(p+8); 
  24.         xfsLoad3 = _mm_load_ps(p+12); 
  25.         xfsSum = _mm_add_ps(xfsSum, xfsLoad);   // [SSE] 單精浮點緊縮加法
  26.         xfsSum1 = _mm_add_ps(xfsSum1, xfsLoad1); 
  27.         xfsSum2 = _mm_add_ps(xfsSum2, xfsLoad2); 
  28.         xfsSum3 = _mm_add_ps(xfsSum3, xfsLoad3); 
  29.         p += nBlockWidth; 
  30.     } 
  31.     // 合併.
  32.     xfsSum = _mm_add_ps(xfsSum, xfsSum1);   // 兩兩合併(0~1).
  33.     xfsSum2 = _mm_add_ps(xfsSum2, xfsSum3); // 兩兩合併(2~3).
  34.     xfsSum = _mm_add_ps(xfsSum, xfsSum2);   // 兩兩合併(0~3).
  35.     q = (constfloat*)&xfsSum; 
  36.     s = q[0] + q[1] + q[2] + q[3]; 
  37.     // 處理剩下的.
  38.     for(i=0; i<cntRem; ++i) 
  39.     { 
  40.         s += p[i]; 
  41.     } 
  42.     return s; 
  1. // 單精度浮點陣列求和_SSE四路迴圈展開版.
  2. float sumfloat_sse_4loop(constfloat* pbuf, size_t cntbuf)  
  3. {  
  4.     float s = 0;    // 返回值.
  5.     size_t i;  
  6.     size_t nBlockWidth = 4*4;   // 塊寬. SSE暫存器能一次處理4個float,然後迴圈展開4次.
  7.     size_t cntBlock = cntbuf / nBlockWidth; // 塊數.
  8.     size_t cntRem = cntbuf % nBlockWidth;   // 剩餘數量.
  9.     __m128 xfsSum = _mm_setzero_ps();   // 求和變數。[SSE] 賦初值0
  10.     __m128 xfsSum1 = _mm_setzero_ps();  
  11.     __m128 xfsSum2 = _mm_setzero_ps();  
  12.     __m128 xfsSum3 = _mm_setzero_ps();  
  13.     __m128 xfsLoad; // 載入.
  14.     __m128 xfsLoad1;  
  15.     __m128 xfsLoad2;  
  16.     __m128 xfsLoad3;  
  17.     constfloat* p = pbuf;  // SSE批量處理時所用的指標.
  18.     constfloat* q; // 將SSE變數上的多個數值合併時所用指標.
  19.     // SSE批量處理.
  20.     for(i=0; i<cntBlock; ++i)  
  21.     {  
  22.         xfsLoad = _mm_load_ps(p);   // [SSE] 載入.
  23.         xfsLoad1 = _mm_load_ps(p+4);  
  24.         xfsLoad2 = _mm_load_ps(p+8);  
  25.         xfsLoad3 = _mm_load_ps(p+12);  
  26.         xfsSum = _mm_add_ps(xfsSum, xfsLoad);   // [SSE] 單精浮點緊縮加法
  27.         xfsSum1 = _mm_add_ps(xfsSum1, xfsLoad1);  
  28.         xfsSum2 = _mm_add_ps(xfsSum2, xfsLoad2);  
  29.         xfsSum3 = _mm_add_ps(xfsSum3, xfsLoad3);  
  30.         p += nBlockWidth;  
  31.     }  
  32.     // 合併.
  33.     xfsSum = _mm_add_ps(xfsSum, xfsSum1);   // 兩兩合併(0~1).
  34.     xfsSum2 = _mm_add_ps(xfsSum2, xfsSum3); // 兩兩合併(2~3).
  35.     xfsSum = _mm_add_ps(xfsSum, xfsSum2);   // 兩兩合併(0~3).
  36.     q = (constfloat*)&xfsSum;  
  37.     s = q[0] + q[1] + q[2] + q[3];  
  38.     // 處理剩下的.
  39.     for(i=0; i<cntRem; ++i)  
  40.     {  
  41.         s += p[i];  
  42.     }  
  43.     return s;  
  44. }  


2.4 AVX版

2.4.1 AVX普通版

  AVX暫存器是256位的,對應__m256型別,它能一次能處理8個單精度浮點數。
  很多AVX指令要求記憶體地址按32位元組對齊。本文為了簡化,假定浮點陣列的首地址是總是32位元組對齊的,僅需要考慮陣列長度不是8的整數倍問題。
  因使用了AVX Intrinsic函式,我們可以根據zintrin.h所提供的INTRIN_AVX巨集進行條件編譯。

  程式碼如下——

  1. #ifdef INTRIN_AVX
  2. // 單精度浮點陣列求和_AVX版.
  3. float sumfloat_avx(constfloat* pbuf, size_t cntbuf) 
  4.     float s = 0;    // 求和變數.
  5.     size_t i; 
  6.     size_t nBlockWidth = 8;// 塊寬. AVX暫存器能一次處理8個float.
  7.     size_t cntBlock = cntbuf / nBlockWidth;// 塊數.
  8.     size_t cntRem = cntbuf % nBlockWidth;  // 剩餘數量.
  9.     __m256 yfsSum = _mm256_setzero_ps();    // 求和變數。[AVX] 賦初值0
  10.     __m256 yfsLoad; // 載入.
  11.     constfloat* p = pbuf; // AVX批量處理時所用的指標.
  12.     constfloat* q;// 將AVX變數上的多個數值合併時所用指標.
  13.     // AVX批量處理.
  14.     for(i=0; i<cntBlock; ++i) 
  15.     { 
  16.         yfsLoad = _mm256_load_ps(p);    // [AVX] 載入
  17.         yfsSum = _mm256_add_ps(yfsSum, yfsLoad);    // [AVX] 單精浮點緊縮加法
  18.         p += nBlockWidth; 
  19.     } 
  20.     // 合併.
  21.     q = (constfloat*)&yfsSum; 
  22.     s = q[0] + q[1] + q[2] + q[3] + q[4] + q[5] + q[6] + q[7]; 
  23.     // 處理剩下的.
  24.     for(i=0; i<cntRem; ++i) 
  25.     { 
  26.         s += p[i]; 
  27.     } 
  28.     return s; 
  29. #endif  // #ifdef INTRIN_AVX
  1. #ifdef INTRIN_AVX
  2. // 單精度浮點陣列求和_AVX版.
  3. float sumfloat_avx(constfloat* pbuf, size_t cntbuf)  
  4. {  
  5.     float s = 0;    // 求和變數.
  6.     size_t i;  
  7.     size_t nBlockWidth = 8; // 塊寬. AVX暫存器能一次處理8個float.
  8.     size_t cntBlock = cntbuf / nBlockWidth; // 塊數.
  9.     size_t cntRem = cntbuf % nBlockWidth;   // 剩餘數量.
  10.     __m256 yfsSum = _mm256_setzero_ps();    // 求和變數。[AVX] 賦初值0
  11.     __m256 yfsLoad; // 載入.
  12.     constfloat* p = pbuf;  // AVX批量處理時所用的指標.
  13.     constfloat* q; // 將AVX變數上的多個數值合併時所用指標.
  14.     // AVX批量處理.
  15.     for(i=0; i<cntBlock; ++i)  
  16.     {  
  17.         yfsLoad = _mm256_load_ps(p);    // [AVX] 載入
  18.         yfsSum = _mm256_add_ps(yfsSum, yfsLoad);    // [AVX] 單精浮點緊縮加法
  19.         p += nBlockWidth;  
  20.     }  
  21.     // 合併.
  22.     q = (constfloat*)&yfsSum;  
  23.     s = q[0] + q[1] + q[2] + q[3] + q[4] + q[5] + q[6] + q[7];  
  24.     // 處理剩下的.
  25.     for(i=0; i<cntRem; ++i)  
  26.     {  
  27.         s += p[i];  
  28.     }  
  29.     return s;  
  30. }  
  31. #endif  // #ifdef INTRIN_AVX

  由上可見,將SSE Intrinsic程式碼(sumfloat_sse)升級為 AVX Intrinsic程式碼(sumfloat_avx)是很容易的——
1. 升級資料型別,將__m128升級成了__m256。
2. 升級Intrinsic函式,在函式名中加入255。例如_mm_setzero_ps、_mm_load_ps、_mm_add_ps,對應的AVX版函式是 _mm256_setzero_ps、_mm256_load_ps、_mm256_add_ps。
3. 因位寬翻倍,地址計算與資料合併的程式碼需稍加改動。

  當使用VC2010編譯含有AVX的程式碼時,VC會提醒你——
warning C4752: 發現 Intel(R) 高階向量擴充套件;請考慮使用 /arch:AVX

  目前“/arch:AVX”尚未整合到專案屬性的“C++\程式碼生成\啟用增強指令集”中,需要手動在專案屬性的“C++\命令列”的附加選項中加上“/arch:AVX”——

詳見MSDN——
http://msdn.microsoft.com/zh-cn/library/7t5yh4fd(v=vs.100).aspx
在 Visual Studio 中設定 /arch:AVX 編譯器選項
1.開啟專案的“屬性頁”對話方塊。 有關更多資訊,請參見 如何:開啟專案屬性頁。
2.單擊“C/C++”資料夾。
3.單擊“命令列”屬性頁。
4.在“附加選項”框中新增 /arch:AVX。


2.4.2 AVX四路迴圈展開版

  同樣的,我們可以編寫AVX四路迴圈展開版——

  1. // 單精度浮點陣列求和_AVX四路迴圈展開版.
  2. float sumfloat_avx_4loop(constfloat* pbuf, size_t cntbuf) 
  3.     float s = 0;    // 求和變數.
  4.     size_t i; 
  5.     size_t nBlockWidth = 8*4;   // 塊寬. AVX暫存器能一次處理8個float,然後迴圈展開4次.
  6.     size_t cntBlock = cntbuf / nBlockWidth;// 塊數.
  7.     size_t cntRem = cntbuf % nBlockWidth;  // 剩餘數量.
  8.     __m256 yfsSum = _mm256_setzero_ps();    // 求和變數。[AVX] 賦初值0
  9.     __m256 yfsSum1 = _mm256_setzero_ps(); 
  10.     __m256 yfsSum2 = _mm256_setzero_ps(); 
  11.     __m256 yfsSum3 = _mm256_setzero_ps(); 
  12.     __m256 yfsLoad; // 載入.
  13.     __m256 yfsLoad1; 
  14.     __m256 yfsLoad2; 
  15.     __m256 yfsLoad3; 
  16.     constfloat* p = pbuf; // AVX批量處理時所用的指標.
  17.     constfloat* q;// 將AVX變數上的多個數值合併時所用指標.
  18.     // AVX批量處理.
  19.     for(i=0; i<cntBlock; ++i) 
  20.     { 
  21.         yfsLoad = _mm256_load_ps(p);    // [AVX] 載入.
  22.         yfsLoad1 = _mm256_load_ps(p+8); 
  23.         yfsLoad2 = _mm256_load_ps(p+16); 
  24.         yfsLoad3 = _mm256_load_ps(p+24); 
  25.         yfsSum = _mm256_add_ps(yfsSum, yfsLoad);    // [AVX] 單精浮點緊縮加法
  26.         yfsSum1 = _mm256_add_ps(yfsSum1, yfsLoad1); 
  27.         yfsSum2 = _mm256_add_ps(yfsSum2, yfsLoad2); 
  28.         yfsSum3 = _mm256_add_ps(yfsSum3, yfsLoad3); 
  29.         p += nBlockWidth; 
  30.     } 
  31.     // 合併.
  32.     yfsSum = _mm256_add_ps(yfsSum, yfsSum1);    // 兩兩合併(0~1).
  33.     yfsSum2 = _mm256_add_ps(yfsSum2, yfsSum3);  // 兩兩合併(2~3).
  34.     yfsSum = _mm256_add_ps(yfsSum, yfsSum2);    // 兩兩合併(0~3).
  35.     q = (constfloat*)&yfsSum; 
  36.     s = q[0] + q[1] + q[2] + q[3] + q[4] + q[5] + q[6] + q[7]; 
  37.     // 處理剩下的.
  38.     for(i=0; i<cntRem; ++i) 
  39.     { 
  40.         s += p[i]; 
  41.     } 
  42.     return s; 
  1. // 單精度浮點陣列求和_AVX四路迴圈展開版.
  2. float sumfloat_avx_4loop(constfloat* pbuf, size_t cntbuf)  
  3. {  
  4.     float s = 0;    // 求和變數.
  5.     size_t i;  
  6.     size_t nBlockWidth = 8*4;   // 塊寬. AVX暫存器能一次處理8個float,然後迴圈展開4次.
  7.     size_t cntBlock = cntbuf / nBlockWidth; // 塊數.
  8.     size_t cntRem = cntbuf % nBlockWidth;   // 剩餘數量.
  9.     __m256 yfsSum = _mm256_setzero_ps();    // 求和變數。[AVX] 賦初值0
  10.     __m256 yfsSum1 = _mm256_setzero_ps();  
  11.     __m256 yfsSum2 = _mm256_setzero_ps();  
  12.     __m256 yfsSum3 = _mm256_setzero_ps();  
  13.     __m256 yfsLoad; // 載入.
  14.     __m256 yfsLoad1;  
  15.     __m256 yfsLoad2;  
  16.     __m256 yfsLoad3;  
  17.     constfloat* p = pbuf;  // AVX批量處理時所用的指標.
  18.     constfloat* q; // 將AVX變數上的多個數值合併時所用指標.
  19.     // AVX批量處理.
  20.     for(i=0; i<cntBlock; ++i)  
  21.     {  
  22.         yfsLoad = _mm256_load_ps(p);    // [AVX] 載入.
  23.         yfsLoad1 = _mm256_load_ps(p+8);  
  24.         yfsLoad2 = _mm256_load_ps(p+16);  
  25.         yfsLoad3 = _mm256_load_ps(p+24);  
  26.         yfsSum = _mm256_add_ps(yfsSum, yfsLoad);    // [AVX] 單精浮點緊縮加法
  27.         yfsSum1 = _mm256_add_ps(yfsSum1, yfsLoad1);  
  28.         yfsSum2 = _mm256_add_ps(yfsSum2, yfsLoad2);  
  29.         yfsSum3 = _mm256_add_ps(yfsSum3, yfsLoad3);  
  30.         p += nBlockWidth;  
  31.     }  
  32.     // 合併.
  33.     yfsSum = _mm256_add_ps(yfsSum, yfsSum1);    // 兩兩合併(0~1).
  34.     yfsSum2 = _mm256_add_ps(yfsSum2, yfsSum3);  // 兩兩合併(2~3).
  35.     yfsSum = _mm256_add_ps(yfsSum, yfsSum2);    // 兩兩合併(0~3).
  36.     q = (constfloat*)&yfsSum;  
  37.     s = q[0] + q[1] + q[2] + q[3] + q[4] + q[5] + q[6] + q[7];  
  38.     // 處理剩下的.
  39.     for(i=0; i<cntRem; ++i)  
  40.     {  
  41.         s += p[i];  
  42.     }  
  43.     return s;  
  44. }  

2.5 測試框架

2.5.1 測試所用的陣列

  首先考慮一下測試所用的陣列的長度應該是多少比較好。
  為了避免記憶體頻寬問題,這個陣列最好能放在L1 Data Cache中。現在的處理器的L1 Data Cache一般是32KB,為了保險最好再除以2,那麼陣列的長度應該是 32KB/(2*sizeof(float))=4096。
  其次考慮記憶體對齊問題,avx要求32位元組對齊。我們可以定義一個ATTR_ALIGN巨集來統一處理變數的記憶體對齊問題。
  該陣列定義如下——

  1. // 變數對齊.
  2. #ifndef ATTR_ALIGN
  3. #  if defined(__GNUC__) // GCC
  4. #    define ATTR_ALIGN(n)   __attribute__((aligned(n)))
  5. #  else // 否則使用VC格式.
  6. #    define ATTR_ALIGN(n)   __declspec(align(n))
  7. #  endif
  8. #endif  // #ifndef ATTR_ALIGN
  9. #define BUFSIZE 4096    // = 32KB{L1 Cache} / (2 * sizeof(float))
  10. ATTR_ALIGN(32) float buf[BUFSIZE]; 
  1. // 變數對齊.
  2. #ifndef ATTR_ALIGN
  3. #  if defined(__GNUC__) // GCC
  4. #    define ATTR_ALIGN(n)   __attribute__((aligned(n)))
  5. #  else // 否則使用VC格式.
  6. #    define ATTR_ALIGN(n)   __declspec(align(n))
  7. #  endif
  8. #endif  // #ifndef ATTR_ALIGN
  9. #define BUFSIZE 4096    // = 32KB{L1 Cache} / (2 * sizeof(float))
  10. ATTR_ALIGN(32) float buf[BUFSIZE];  


2.5.2 測試函式

  如果為每一個函式都編寫一套測試程式碼,那不僅程式碼量大,而且不易維護。
  可以考慮利用函式指標來實現一套測試框架。
  因sumfloat_base等函式的簽名是一致的,於是可以定義這樣的一種函式指標——
// 測試時的函式型別
typedef float (*TESTPROC)(const float* pbuf, size_t cntbuf);

  然後再編寫一個對TESTPROC函式指標進行測試的函式——

  1. // 進行測試
  2. void runTest(constchar* szname, TESTPROC proc) 
  3.     constint testloop = 4000; // 重複運算幾次延長時間,避免計時精度問題.
  4.     constclock_t TIMEOUT = CLOCKS_PER_SEC/2;  // 最短測試時間.
  5.     int i,j,k; 
  6.     clock_t tm0, dt;   // 儲存時間.
  7.     double mps; // M/s.
  8.     double mps_good = 0;   // 最佳M/s. 因執行緒切換會導致的數值波動, 於是選取最佳值.
  9.     volatilefloat n=0;// 避免內迴圈被優化.
  10.     for(i=1; i<=3; ++i) // 多次測試.
  11.     { 
  12.         tm0 = clock(); 
  13.         // main
  14.         k=0; 
  15.         do
  16.         { 
  17.             for(j=1; j<=testloop; ++j)  // 重複運算幾次延長時間,避免計時開銷帶來的影響.
  18.             { 
  19.                 n = proc(buf, BUFSIZE); // 避免內迴圈被編譯優化消掉.
  20.             } 
  21.             ++k; 
  22.             dt = clock() - tm0; 
  23.         }while(dt<TIMEOUT); 
  24.         // show
  25.         mps = (double)k*testloop*BUFSIZE*CLOCKS_PER_SEC/(1024.0*1024.0*dt);// k*testloop*BUFSIZE/(1024.0*1024.0) 將資料規模換算為M,然後再乘以 CLOCKS_PER_SEC/dt 換算為M/s .
  26.         if (mps_good<mps)    mps_good=mps;  // 選取最佳值.
  27.         //printf("%s:\t%.0f M/s\t//%f\n", szname, mps, n);
  28.     } 
  29.     printf("%s:\t%.0f M/s\t//%f\n", szname, mps_good, n); 
  1. // 進行測試
  2. void runTest(constchar* szname, TESTPROC proc)  
  3. {  
  4.     constint testloop = 4000;  // 重複運算幾次延長時間,避免計時精度問題.
  5.     constclock_t TIMEOUT = CLOCKS_PER_SEC/2;   // 最短測試時間.
  6.     int i,j,k;  
  7.     clock_t tm0, dt;    // 儲存時間.
  8.     double mps; // M/s.
  9.     double mps_good = 0;    // 最佳M/s. 因執行緒切換會導致的數值波動, 於是選取最佳值.
  10.     volatilefloat n=0; // 避免內迴圈被優化.
  11.     for(i=1; i<=3; ++i)  // 多次測試.
  12.     {  
  13.         tm0 = clock();  
  14.         // main
  15.         k=0;  
  16.         do
  17.         {  
  18.             for(j=1; j<=testloop; ++j)   // 重複運算幾次延長時間,避免計時開銷帶來的影響.
  19.             {  
  20.                 n = proc(buf, BUFSIZE); // 避免內迴圈被編譯優化消掉.
  21.             }  
  22.             ++k;  
  23.             dt = clock() - tm0;  
  24.         }while(dt<TIMEOUT);  
  25.         // show
  26.         mps = (double)k*testloop*BUFSIZE*CLOCKS_PER_SEC/(1024.0*1024.0*dt); // k*testloop*BUFSIZE/(1024.0*1024.0) 將資料規模換算為M,然後再乘以 CLOCKS_PER_SEC/dt 換算為M/s .
  27.         if (mps_good<mps)    mps_good=mps;   // 選取最佳值.
  28.         //printf("%s:\t%.0f M/s\t//%f\n", szname, mps, n);
  29.     }  
  30.     printf("%s:\t%.0f M/s\t//%f\n", szname, mps_good, n);  
  31. }  


  j是最內層的迴圈,負責多次呼叫TESTPROC函式指標。如果每呼叫一次TESTPROC函式指標後又呼叫clock函式,那會帶來較大的計時開銷,影響評測成績。
  k迴圈負責檢測超時。當發現超過預定時限,便計算mps,即每秒鐘處理了多少百萬個單精度浮點數。然後儲存最佳的mps。
  i是最外層迴圈的迴圈變數,迴圈3次然後報告最佳值。


2.5.3 進行測試

  在進行測試之前,需要對buf陣列進行初始化,將陣列元素賦隨機值——

  1. // init buf
  2. srand( (unsigned)time( NULL ) ); 
  3. for (i = 0; i < BUFSIZE; i++) buf[i] = (float)(rand() & 0x3f);  // 使用&0x3f是為了讓求和後的數值不會超過float型別的有效位數,便於觀察結果是否正確.
  1. // init buf
  2. srand( (unsigned)time( NULL ) );  
  3. for (i = 0; i < BUFSIZE; i++) buf[i] = (float)(rand() & 0x3f);   // 使用&0x3f是為了讓求和後的數值不會超過float型別的有效位數,便於觀察結果是否正確.

  然後可以開始測試了——

  1. // test
  2.     runTest("sumfloat_base", sumfloat_base);   // 單精度浮點陣列求和_基本版.
  3. #ifdef INTRIN_SSE
  4.     if (simd_sse_level(NULL) >= SIMD_SSE_1) 
  5.     { 
  6.         runTest("sumfloat_sse", sumfloat_sse); // 單精度浮點陣列求和_SSE版.
  7.         runTest("sumfloat_sse_4loop", sumfloat_sse_4loop); // 單精度浮點陣列求和_SSE四路迴圈展開版.
  8.     } 
  9. #endif  // #ifdef INTRIN_SSE
  10. #ifdef INTRIN_AVX
  11.     if (simd_avx_level(NULL) >= SIMD_AVX_1) 
  12.     { 
  13.         runTest("sumfloat_avx", sumfloat_avx); // 單精度浮點陣列求和_SSE版.
  14.         runTest("sumfloat_avx_4loop", sumfloat_avx_4loop); // 單精度浮點陣列求和_SSE四路迴圈展開版.
  15.     } 
  16. #endif  // #ifdef INTRIN_AVX
  1. // test
  2.     runTest("sumfloat_base", sumfloat_base);    // 單精度浮點陣列求和_基本版.
  3. #ifdef INTRIN_SSE
  4.     if (simd_sse_level(NULL) >= SIMD_SSE_1)  
  5.     {  
  6.         runTest("sumfloat_sse", sumfloat_sse);  // 單精度浮點陣列求和_SSE版.
  7.         runTest("sumfloat_sse_4loop", sumfloat_sse_4loop);  // 單精度浮點陣列求和_SSE四路迴圈展開版.
  8.     }  
  9. #endif  // #ifdef INTRIN_SSE
  10. #ifdef INTRIN_AVX
  11.     if (simd_avx_level(NULL) >= SIMD_AVX_1)  
  12.     {  
  13.         runTest("sumfloat_avx", sumfloat_avx);  // 單精度浮點陣列求和_SSE版.
  14.         runTest("sumfloat_avx_4loop", sumfloat_avx_4loop);  // 單精度浮點陣列求和_SSE四路迴圈展開版.
  15.     }  
  16. #endif  // #ifdef INTRIN_AVX


2.6 雜項

  為了方便對比測試,可以在程式啟動時顯示程式版本、編譯器名稱、CPU型號資訊。即在main函式中加上——

  1. char szBuf[64]; 
  2. int i; 
  3. printf("simdsumfloat v1.00 (%dbit)\n", INTRIN_WORDSIZE); 
  4. printf("Compiler: %s\n", COMPILER_NAME); 
  5. cpu_getbrand(szBuf); 
  6. printf("CPU:\t%s\n", szBuf); 
  7. printf("\n"); 
  1. char szBuf[64];  
  2. int i;  
  3. printf("simdsumfloat v1.00 (%dbit)\n", INTRIN_WORDSIZE);  
  4. printf("Compiler: %s\n", COMPILER_NAME);  
  5. cpu_getbrand(szBuf);  
  6. printf("CPU:\t%s\n", szBuf);  
  7. printf("\n");  

  INTRIN_WORDSIZE 巨集是 zintrin.h 提供的,為當前機器的字長。
  cpu_getbrand是 ccpuid.h 提供的,用於獲得CPU型號字串。
  COMPILER_NAME 是一個用來獲得編譯器名稱的巨集,它的詳細定義是——

  1. // Compiler name
  2. #define MACTOSTR(x) #x
  3. #define MACROVALUESTR(x)    MACTOSTR(x)
  4. #if defined(__ICL)  // Intel C++
  5. #  if defined(__VERSION__)
  6. #    define COMPILER_NAME   "Intel C++ " __VERSION__
  7. #  elif defined(__INTEL_COMPILER_BUILD_DATE)
  8. #    define COMPILER_NAME   "Intel C++ (" MACROVALUESTR(__INTEL_COMPILER_BUILD_DATE) ")"
  9. #  else
  10. #    define COMPILER_NAME   "Intel C++"
  11. #  endif    // #  if defined(__VERSION__)
  12. #elif defined(_MSC_VER) // Microsoft VC++
  13. #  if defined(_MSC_FULL_VER)
  14. #    define COMPILER_NAME   "Microsoft VC++ (" MACROVALUESTR(_MSC_FULL_VER) ")"
  15. #  elif defined(_MSC_VER)
  16. #    define COMPILER_NAME   "Microsoft VC++ (" MACROVALUESTR(_MSC_VER) ")"
  17. #  else
  18. #    define COMPILER_NAME