跨平臺SSE、AVX指令測試
一、問題背景
最初,我們只能使用匯編語言來編寫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就行了。例如——
- #define __STDC_LIMIT_MACROS 1 // C99整數範圍常量. [純C程式可以不用, 而C++程式必須定義該巨集.]
- #include "zintrin.h"
- #include "ccpuid.h"
- #define __STDC_LIMIT_MACROS 1 // C99整數範圍常量. [純C程式可以不用, 而C++程式必須定義該巨集.]
- #include "zintrin.h"
- #include "ccpuid.h"
因為stdint.h會被zintrin.h或ccpuid.h引用,所以不需要手動引入它。
因為它們用到了C99整數範圍常量,所以應該在程式的最前面定義__STDC_LIMIT_MACROS巨集(或者可以在專案配置、編譯器命令列等位置進行配置)。根據C99規範,純C程式可以不用, 而C++程式必須定義該巨集。本文為了演示,定義了該巨集。
2.2 C語言版
我們先用C語言編寫一個基本的單精度浮點陣列求和函式——
- // 單精度浮點陣列求和_基本版.
- //
- // result: 返回陣列求和結果.
- // pbuf: 陣列的首地址.
- // cntbuf: 陣列長度.
- float sumfloat_base(constfloat* pbuf, size_t cntbuf)
- {
- float s = 0; // 求和變數.
- size_t i;
- for(i=0; i<cntbuf; ++i)
- {
- s += pbuf[i];
- }
- return s;
- }
- // 單精度浮點陣列求和_基本版.
- //
- // result: 返回陣列求和結果.
- // pbuf: 陣列的首地址.
- // cntbuf: 陣列長度.
- float sumfloat_base(constfloat* pbuf, size_t cntbuf)
- {
- float s = 0; // 求和變數.
- size_t i;
- for(i=0; i<cntbuf; ++i)
- {
- s += pbuf[i];
- }
- return s;
- }
該函式很容易理解——先將返回值賦初值0,然後迴圈加上陣列中每一項的值。
2.3 SSE版
2.3.1 SSE普通版
SSE暫存器是128位的,對應__m128型別,它能一次能處理4個單精度浮點數。
很多SSE指令要求記憶體地址按16位元組對齊。本文為了簡化,假定浮點陣列的首地址是總是16位元組對齊的,僅需要考慮陣列長度不是4的整數倍問題。
因使用了SSE Intrinsic函式,我們可以根據zintrin.h所提供的INTRIN_SSE巨集進行條件編譯。
程式碼如下——
- #ifdef INTRIN_SSE
- // 單精度浮點陣列求和_SSE版.
- float sumfloat_sse(constfloat* pbuf, size_t cntbuf)
- {
- float s = 0; // 求和變數.
- size_t i;
- size_t nBlockWidth = 4;// 塊寬. SSE暫存器能一次處理4個float.
- size_t cntBlock = cntbuf / nBlockWidth;// 塊數.
- size_t cntRem = cntbuf % nBlockWidth; // 剩餘數量.
- __m128 xfsSum = _mm_setzero_ps(); // 求和變數。[SSE] 賦初值0
- __m128 xfsLoad; // 載入.
- constfloat* p = pbuf; // SSE批量處理時所用的指標.
- constfloat* q;// 將SSE變數上的多個數值合併時所用指標.
- // SSE批量處理.
- for(i=0; i<cntBlock; ++i)
- {
- xfsLoad = _mm_load_ps(p); // [SSE] 載入
- xfsSum = _mm_add_ps(xfsSum, xfsLoad); // [SSE] 單精浮點緊縮加法
- p += nBlockWidth;
- }
- // 合併.
- q = (constfloat*)&xfsSum;
- s = q[0] + q[1] + q[2] + q[3];
- // 處理剩下的.
- for(i=0; i<cntRem; ++i)
- {
- s += p[i];
- }
- return s;
- }
- #endif // #ifdef INTRIN_SSE
- #ifdef INTRIN_SSE
- // 單精度浮點陣列求和_SSE版.
- float sumfloat_sse(constfloat* pbuf, size_t cntbuf)
- {
- float s = 0; // 求和變數.
- size_t i;
- size_t nBlockWidth = 4; // 塊寬. SSE暫存器能一次處理4個float.
- size_t cntBlock = cntbuf / nBlockWidth; // 塊數.
- size_t cntRem = cntbuf % nBlockWidth; // 剩餘數量.
- __m128 xfsSum = _mm_setzero_ps(); // 求和變數。[SSE] 賦初值0
- __m128 xfsLoad; // 載入.
- constfloat* p = pbuf; // SSE批量處理時所用的指標.
- constfloat* q; // 將SSE變數上的多個數值合併時所用指標.
- // SSE批量處理.
- for(i=0; i<cntBlock; ++i)
- {
- xfsLoad = _mm_load_ps(p); // [SSE] 載入
- xfsSum = _mm_add_ps(xfsSum, xfsLoad); // [SSE] 單精浮點緊縮加法
- p += nBlockWidth;
- }
- // 合併.
- q = (constfloat*)&xfsSum;
- s = q[0] + q[1] + q[2] + q[3];
- // 處理剩下的.
- for(i=0; i<cntRem; ++i)
- {
- s += p[i];
- }
- return s;
- }
- #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四路迴圈展開版
迴圈展開可以降低迴圈開銷,提高指令級並行效能。
一般來說,四路迴圈展開就差不多夠了。我們可以很方便的將上一節的程式碼改造為四路迴圈展開版——
- // 單精度浮點陣列求和_SSE四路迴圈展開版.
- float sumfloat_sse_4loop(constfloat* pbuf, size_t cntbuf)
- {
- float s = 0; // 返回值.
- size_t i;
- size_t nBlockWidth = 4*4; // 塊寬. SSE暫存器能一次處理4個float,然後迴圈展開4次.
- size_t cntBlock = cntbuf / nBlockWidth;// 塊數.
- size_t cntRem = cntbuf % nBlockWidth; // 剩餘數量.
- __m128 xfsSum = _mm_setzero_ps(); // 求和變數。[SSE] 賦初值0
- __m128 xfsSum1 = _mm_setzero_ps();
- __m128 xfsSum2 = _mm_setzero_ps();
- __m128 xfsSum3 = _mm_setzero_ps();
- __m128 xfsLoad; // 載入.
- __m128 xfsLoad1;
- __m128 xfsLoad2;
- __m128 xfsLoad3;
- constfloat* p = pbuf; // SSE批量處理時所用的指標.
- constfloat* q;// 將SSE變數上的多個數值合併時所用指標.
- // SSE批量處理.
- for(i=0; i<cntBlock; ++i)
- {
- xfsLoad = _mm_load_ps(p); // [SSE] 載入.
- xfsLoad1 = _mm_load_ps(p+4);
- xfsLoad2 = _mm_load_ps(p+8);
- xfsLoad3 = _mm_load_ps(p+12);
- xfsSum = _mm_add_ps(xfsSum, xfsLoad); // [SSE] 單精浮點緊縮加法
- xfsSum1 = _mm_add_ps(xfsSum1, xfsLoad1);
- xfsSum2 = _mm_add_ps(xfsSum2, xfsLoad2);
- xfsSum3 = _mm_add_ps(xfsSum3, xfsLoad3);
- p += nBlockWidth;
- }
- // 合併.
- xfsSum = _mm_add_ps(xfsSum, xfsSum1); // 兩兩合併(0~1).
- xfsSum2 = _mm_add_ps(xfsSum2, xfsSum3); // 兩兩合併(2~3).
- xfsSum = _mm_add_ps(xfsSum, xfsSum2); // 兩兩合併(0~3).
- q = (constfloat*)&xfsSum;
- s = q[0] + q[1] + q[2] + q[3];
- // 處理剩下的.
- for(i=0; i<cntRem; ++i)
- {
- s += p[i];
- }
- return s;
- }
- // 單精度浮點陣列求和_SSE四路迴圈展開版.
- float sumfloat_sse_4loop(constfloat* pbuf, size_t cntbuf)
- {
- float s = 0; // 返回值.
- size_t i;
- size_t nBlockWidth = 4*4; // 塊寬. SSE暫存器能一次處理4個float,然後迴圈展開4次.
- size_t cntBlock = cntbuf / nBlockWidth; // 塊數.
- size_t cntRem = cntbuf % nBlockWidth; // 剩餘數量.
- __m128 xfsSum = _mm_setzero_ps(); // 求和變數。[SSE] 賦初值0
- __m128 xfsSum1 = _mm_setzero_ps();
- __m128 xfsSum2 = _mm_setzero_ps();
- __m128 xfsSum3 = _mm_setzero_ps();
- __m128 xfsLoad; // 載入.
- __m128 xfsLoad1;
- __m128 xfsLoad2;
- __m128 xfsLoad3;
- constfloat* p = pbuf; // SSE批量處理時所用的指標.
- constfloat* q; // 將SSE變數上的多個數值合併時所用指標.
- // SSE批量處理.
- for(i=0; i<cntBlock; ++i)
- {
- xfsLoad = _mm_load_ps(p); // [SSE] 載入.
- xfsLoad1 = _mm_load_ps(p+4);
- xfsLoad2 = _mm_load_ps(p+8);
- xfsLoad3 = _mm_load_ps(p+12);
- xfsSum = _mm_add_ps(xfsSum, xfsLoad); // [SSE] 單精浮點緊縮加法
- xfsSum1 = _mm_add_ps(xfsSum1, xfsLoad1);
- xfsSum2 = _mm_add_ps(xfsSum2, xfsLoad2);
- xfsSum3 = _mm_add_ps(xfsSum3, xfsLoad3);
- p += nBlockWidth;
- }
- // 合併.
- xfsSum = _mm_add_ps(xfsSum, xfsSum1); // 兩兩合併(0~1).
- xfsSum2 = _mm_add_ps(xfsSum2, xfsSum3); // 兩兩合併(2~3).
- xfsSum = _mm_add_ps(xfsSum, xfsSum2); // 兩兩合併(0~3).
- q = (constfloat*)&xfsSum;
- s = q[0] + q[1] + q[2] + q[3];
- // 處理剩下的.
- for(i=0; i<cntRem; ++i)
- {
- s += p[i];
- }
- return s;
- }
2.4 AVX版
2.4.1 AVX普通版
AVX暫存器是256位的,對應__m256型別,它能一次能處理8個單精度浮點數。
很多AVX指令要求記憶體地址按32位元組對齊。本文為了簡化,假定浮點陣列的首地址是總是32位元組對齊的,僅需要考慮陣列長度不是8的整數倍問題。
因使用了AVX Intrinsic函式,我們可以根據zintrin.h所提供的INTRIN_AVX巨集進行條件編譯。
程式碼如下——
- #ifdef INTRIN_AVX
- // 單精度浮點陣列求和_AVX版.
- float sumfloat_avx(constfloat* pbuf, size_t cntbuf)
- {
- float s = 0; // 求和變數.
- size_t i;
- size_t nBlockWidth = 8;// 塊寬. AVX暫存器能一次處理8個float.
- size_t cntBlock = cntbuf / nBlockWidth;// 塊數.
- size_t cntRem = cntbuf % nBlockWidth; // 剩餘數量.
- __m256 yfsSum = _mm256_setzero_ps(); // 求和變數。[AVX] 賦初值0
- __m256 yfsLoad; // 載入.
- constfloat* p = pbuf; // AVX批量處理時所用的指標.
- constfloat* q;// 將AVX變數上的多個數值合併時所用指標.
- // AVX批量處理.
- for(i=0; i<cntBlock; ++i)
- {
- yfsLoad = _mm256_load_ps(p); // [AVX] 載入
- yfsSum = _mm256_add_ps(yfsSum, yfsLoad); // [AVX] 單精浮點緊縮加法
- p += nBlockWidth;
- }
- // 合併.
- q = (constfloat*)&yfsSum;
- s = q[0] + q[1] + q[2] + q[3] + q[4] + q[5] + q[6] + q[7];
- // 處理剩下的.
- for(i=0; i<cntRem; ++i)
- {
- s += p[i];
- }
- return s;
- }
- #endif // #ifdef INTRIN_AVX
- #ifdef INTRIN_AVX
- // 單精度浮點陣列求和_AVX版.
- float sumfloat_avx(constfloat* pbuf, size_t cntbuf)
- {
- float s = 0; // 求和變數.
- size_t i;
- size_t nBlockWidth = 8; // 塊寬. AVX暫存器能一次處理8個float.
- size_t cntBlock = cntbuf / nBlockWidth; // 塊數.
- size_t cntRem = cntbuf % nBlockWidth; // 剩餘數量.
- __m256 yfsSum = _mm256_setzero_ps(); // 求和變數。[AVX] 賦初值0
- __m256 yfsLoad; // 載入.
- constfloat* p = pbuf; // AVX批量處理時所用的指標.
- constfloat* q; // 將AVX變數上的多個數值合併時所用指標.
- // AVX批量處理.
- for(i=0; i<cntBlock; ++i)
- {
- yfsLoad = _mm256_load_ps(p); // [AVX] 載入
- yfsSum = _mm256_add_ps(yfsSum, yfsLoad); // [AVX] 單精浮點緊縮加法
- p += nBlockWidth;
- }
- // 合併.
- q = (constfloat*)&yfsSum;
- s = q[0] + q[1] + q[2] + q[3] + q[4] + q[5] + q[6] + q[7];
- // 處理剩下的.
- for(i=0; i<cntRem; ++i)
- {
- s += p[i];
- }
- return s;
- }
- #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四路迴圈展開版——
- // 單精度浮點陣列求和_AVX四路迴圈展開版.
- float sumfloat_avx_4loop(constfloat* pbuf, size_t cntbuf)
- {
- float s = 0; // 求和變數.
- size_t i;
- size_t nBlockWidth = 8*4; // 塊寬. AVX暫存器能一次處理8個float,然後迴圈展開4次.
- size_t cntBlock = cntbuf / nBlockWidth;// 塊數.
- size_t cntRem = cntbuf % nBlockWidth; // 剩餘數量.
- __m256 yfsSum = _mm256_setzero_ps(); // 求和變數。[AVX] 賦初值0
- __m256 yfsSum1 = _mm256_setzero_ps();
- __m256 yfsSum2 = _mm256_setzero_ps();
- __m256 yfsSum3 = _mm256_setzero_ps();
- __m256 yfsLoad; // 載入.
- __m256 yfsLoad1;
- __m256 yfsLoad2;
- __m256 yfsLoad3;
- constfloat* p = pbuf; // AVX批量處理時所用的指標.
- constfloat* q;// 將AVX變數上的多個數值合併時所用指標.
- // AVX批量處理.
- for(i=0; i<cntBlock; ++i)
- {
- yfsLoad = _mm256_load_ps(p); // [AVX] 載入.
- yfsLoad1 = _mm256_load_ps(p+8);
- yfsLoad2 = _mm256_load_ps(p+16);
- yfsLoad3 = _mm256_load_ps(p+24);
- yfsSum = _mm256_add_ps(yfsSum, yfsLoad); // [AVX] 單精浮點緊縮加法
- yfsSum1 = _mm256_add_ps(yfsSum1, yfsLoad1);
- yfsSum2 = _mm256_add_ps(yfsSum2, yfsLoad2);
- yfsSum3 = _mm256_add_ps(yfsSum3, yfsLoad3);
- p += nBlockWidth;
- }
- // 合併.
- yfsSum = _mm256_add_ps(yfsSum, yfsSum1); // 兩兩合併(0~1).
- yfsSum2 = _mm256_add_ps(yfsSum2, yfsSum3); // 兩兩合併(2~3).
- yfsSum = _mm256_add_ps(yfsSum, yfsSum2); // 兩兩合併(0~3).
- q = (constfloat*)&yfsSum;
- s = q[0] + q[1] + q[2] + q[3] + q[4] + q[5] + q[6] + q[7];
- // 處理剩下的.
- for(i=0; i<cntRem; ++i)
- {
- s += p[i];
- }
- return s;
- }
- // 單精度浮點陣列求和_AVX四路迴圈展開版.
- float sumfloat_avx_4loop(constfloat* pbuf, size_t cntbuf)
- {
- float s = 0; // 求和變數.
- size_t i;
- size_t nBlockWidth = 8*4; // 塊寬. AVX暫存器能一次處理8個float,然後迴圈展開4次.
- size_t cntBlock = cntbuf / nBlockWidth; // 塊數.
- size_t cntRem = cntbuf % nBlockWidth; // 剩餘數量.
- __m256 yfsSum = _mm256_setzero_ps(); // 求和變數。[AVX] 賦初值0
- __m256 yfsSum1 = _mm256_setzero_ps();
- __m256 yfsSum2 = _mm256_setzero_ps();
- __m256 yfsSum3 = _mm256_setzero_ps();
- __m256 yfsLoad; // 載入.
- __m256 yfsLoad1;
- __m256 yfsLoad2;
- __m256 yfsLoad3;
- constfloat* p = pbuf; // AVX批量處理時所用的指標.
- constfloat* q; // 將AVX變數上的多個數值合併時所用指標.
- // AVX批量處理.
- for(i=0; i<cntBlock; ++i)
- {
- yfsLoad = _mm256_load_ps(p); // [AVX] 載入.
- yfsLoad1 = _mm256_load_ps(p+8);
- yfsLoad2 = _mm256_load_ps(p+16);
- yfsLoad3 = _mm256_load_ps(p+24);
- yfsSum = _mm256_add_ps(yfsSum, yfsLoad); // [AVX] 單精浮點緊縮加法
- yfsSum1 = _mm256_add_ps(yfsSum1, yfsLoad1);
- yfsSum2 = _mm256_add_ps(yfsSum2, yfsLoad2);
- yfsSum3 = _mm256_add_ps(yfsSum3, yfsLoad3);
- p += nBlockWidth;
- }
- // 合併.
- yfsSum = _mm256_add_ps(yfsSum, yfsSum1); // 兩兩合併(0~1).
- yfsSum2 = _mm256_add_ps(yfsSum2, yfsSum3); // 兩兩合併(2~3).
- yfsSum = _mm256_add_ps(yfsSum, yfsSum2); // 兩兩合併(0~3).
- q = (constfloat*)&yfsSum;
- s = q[0] + q[1] + q[2] + q[3] + q[4] + q[5] + q[6] + q[7];
- // 處理剩下的.
- for(i=0; i<cntRem; ++i)
- {
- s += p[i];
- }
- return s;
- }
2.5 測試框架
2.5.1 測試所用的陣列
首先考慮一下測試所用的陣列的長度應該是多少比較好。
為了避免記憶體頻寬問題,這個陣列最好能放在L1 Data Cache中。現在的處理器的L1 Data Cache一般是32KB,為了保險最好再除以2,那麼陣列的長度應該是 32KB/(2*sizeof(float))=4096。
其次考慮記憶體對齊問題,avx要求32位元組對齊。我們可以定義一個ATTR_ALIGN巨集來統一處理變數的記憶體對齊問題。
該陣列定義如下——
- // 變數對齊.
- #ifndef ATTR_ALIGN
- # if defined(__GNUC__) // GCC
- # define ATTR_ALIGN(n) __attribute__((aligned(n)))
- # else // 否則使用VC格式.
- # define ATTR_ALIGN(n) __declspec(align(n))
- # endif
- #endif // #ifndef ATTR_ALIGN
- #define BUFSIZE 4096 // = 32KB{L1 Cache} / (2 * sizeof(float))
- ATTR_ALIGN(32) float buf[BUFSIZE];
- // 變數對齊.
- #ifndef ATTR_ALIGN
- # if defined(__GNUC__) // GCC
- # define ATTR_ALIGN(n) __attribute__((aligned(n)))
- # else // 否則使用VC格式.
- # define ATTR_ALIGN(n) __declspec(align(n))
- # endif
- #endif // #ifndef ATTR_ALIGN
- #define BUFSIZE 4096 // = 32KB{L1 Cache} / (2 * sizeof(float))
- ATTR_ALIGN(32) float buf[BUFSIZE];
2.5.2 測試函式
如果為每一個函式都編寫一套測試程式碼,那不僅程式碼量大,而且不易維護。
可以考慮利用函式指標來實現一套測試框架。
因sumfloat_base等函式的簽名是一致的,於是可以定義這樣的一種函式指標——
// 測試時的函式型別
typedef float (*TESTPROC)(const float* pbuf, size_t cntbuf);
然後再編寫一個對TESTPROC函式指標進行測試的函式——
- // 進行測試
- void runTest(constchar* szname, TESTPROC proc)
- {
- constint testloop = 4000; // 重複運算幾次延長時間,避免計時精度問題.
- constclock_t TIMEOUT = CLOCKS_PER_SEC/2; // 最短測試時間.
- int i,j,k;
- clock_t tm0, dt; // 儲存時間.
- double mps; // M/s.
- double mps_good = 0; // 最佳M/s. 因執行緒切換會導致的數值波動, 於是選取最佳值.
- volatilefloat n=0;// 避免內迴圈被優化.
- for(i=1; i<=3; ++i) // 多次測試.
- {
- tm0 = clock();
- // main
- k=0;
- do
- {
- for(j=1; j<=testloop; ++j) // 重複運算幾次延長時間,避免計時開銷帶來的影響.
- {
- n = proc(buf, BUFSIZE); // 避免內迴圈被編譯優化消掉.
- }
- ++k;
- dt = clock() - tm0;
- }while(dt<TIMEOUT);
- // show
- 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 .
- if (mps_good<mps) mps_good=mps; // 選取最佳值.
- //printf("%s:\t%.0f M/s\t//%f\n", szname, mps, n);
- }
- printf("%s:\t%.0f M/s\t//%f\n", szname, mps_good, n);
- }
- // 進行測試
- void runTest(constchar* szname, TESTPROC proc)
- {
- constint testloop = 4000; // 重複運算幾次延長時間,避免計時精度問題.
- constclock_t TIMEOUT = CLOCKS_PER_SEC/2; // 最短測試時間.
- int i,j,k;
- clock_t tm0, dt; // 儲存時間.
- double mps; // M/s.
- double mps_good = 0; // 最佳M/s. 因執行緒切換會導致的數值波動, 於是選取最佳值.
- volatilefloat n=0; // 避免內迴圈被優化.
- for(i=1; i<=3; ++i) // 多次測試.
- {
- tm0 = clock();
- // main
- k=0;
- do
- {
- for(j=1; j<=testloop; ++j) // 重複運算幾次延長時間,避免計時開銷帶來的影響.
- {
- n = proc(buf, BUFSIZE); // 避免內迴圈被編譯優化消掉.
- }
- ++k;
- dt = clock() - tm0;
- }while(dt<TIMEOUT);
- // show
- 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 .
- if (mps_good<mps) mps_good=mps; // 選取最佳值.
- //printf("%s:\t%.0f M/s\t//%f\n", szname, mps, n);
- }
- printf("%s:\t%.0f M/s\t//%f\n", szname, mps_good, n);
- }
j是最內層的迴圈,負責多次呼叫TESTPROC函式指標。如果每呼叫一次TESTPROC函式指標後又呼叫clock函式,那會帶來較大的計時開銷,影響評測成績。
k迴圈負責檢測超時。當發現超過預定時限,便計算mps,即每秒鐘處理了多少百萬個單精度浮點數。然後儲存最佳的mps。
i是最外層迴圈的迴圈變數,迴圈3次然後報告最佳值。
2.5.3 進行測試
在進行測試之前,需要對buf陣列進行初始化,將陣列元素賦隨機值——
- // init buf
- srand( (unsigned)time( NULL ) );
- for (i = 0; i < BUFSIZE; i++) buf[i] = (float)(rand() & 0x3f); // 使用&0x3f是為了讓求和後的數值不會超過float型別的有效位數,便於觀察結果是否正確.
- // init buf
- srand( (unsigned)time( NULL ) );
- for (i = 0; i < BUFSIZE; i++) buf[i] = (float)(rand() & 0x3f); // 使用&0x3f是為了讓求和後的數值不會超過float型別的有效位數,便於觀察結果是否正確.
然後可以開始測試了——
- // test
- runTest("sumfloat_base", sumfloat_base); // 單精度浮點陣列求和_基本版.
- #ifdef INTRIN_SSE
- if (simd_sse_level(NULL) >= SIMD_SSE_1)
- {
- runTest("sumfloat_sse", sumfloat_sse); // 單精度浮點陣列求和_SSE版.
- runTest("sumfloat_sse_4loop", sumfloat_sse_4loop); // 單精度浮點陣列求和_SSE四路迴圈展開版.
- }
- #endif // #ifdef INTRIN_SSE
- #ifdef INTRIN_AVX
- if (simd_avx_level(NULL) >= SIMD_AVX_1)
- {
- runTest("sumfloat_avx", sumfloat_avx); // 單精度浮點陣列求和_SSE版.
- runTest("sumfloat_avx_4loop", sumfloat_avx_4loop); // 單精度浮點陣列求和_SSE四路迴圈展開版.
- }
- #endif // #ifdef INTRIN_AVX
- // test
- runTest("sumfloat_base", sumfloat_base); // 單精度浮點陣列求和_基本版.
- #ifdef INTRIN_SSE
- if (simd_sse_level(NULL) >= SIMD_SSE_1)
- {
- runTest("sumfloat_sse", sumfloat_sse); // 單精度浮點陣列求和_SSE版.
- runTest("sumfloat_sse_4loop", sumfloat_sse_4loop); // 單精度浮點陣列求和_SSE四路迴圈展開版.
- }
- #endif // #ifdef INTRIN_SSE
- #ifdef INTRIN_AVX
- if (simd_avx_level(NULL) >= SIMD_AVX_1)
- {
- runTest("sumfloat_avx", sumfloat_avx); // 單精度浮點陣列求和_SSE版.
- runTest("sumfloat_avx_4loop", sumfloat_avx_4loop); // 單精度浮點陣列求和_SSE四路迴圈展開版.
- }
- #endif // #ifdef INTRIN_AVX
2.6 雜項
為了方便對比測試,可以在程式啟動時顯示程式版本、編譯器名稱、CPU型號資訊。即在main函式中加上——
- char szBuf[64];
- int i;
- printf("simdsumfloat v1.00 (%dbit)\n", INTRIN_WORDSIZE);
- printf("Compiler: %s\n", COMPILER_NAME);
- cpu_getbrand(szBuf);
- printf("CPU:\t%s\n", szBuf);
- printf("\n");
- char szBuf[64];
- int i;
- printf("simdsumfloat v1.00 (%dbit)\n", INTRIN_WORDSIZE);
- printf("Compiler: %s\n", COMPILER_NAME);
- cpu_getbrand(szBuf);
- printf("CPU:\t%s\n", szBuf);
- printf("\n");
INTRIN_WORDSIZE 巨集是 zintrin.h 提供的,為當前機器的字長。
cpu_getbrand是 ccpuid.h 提供的,用於獲得CPU型號字串。
COMPILER_NAME 是一個用來獲得編譯器名稱的巨集,它的詳細定義是——
- // Compiler name
- #define MACTOSTR(x) #x
- #define MACROVALUESTR(x) MACTOSTR(x)
- #if defined(__ICL) // Intel C++
- # if defined(__VERSION__)
- # define COMPILER_NAME "Intel C++ " __VERSION__
- # elif defined(__INTEL_COMPILER_BUILD_DATE)
- # define COMPILER_NAME "Intel C++ (" MACROVALUESTR(__INTEL_COMPILER_BUILD_DATE) ")"
- # else
- # define COMPILER_NAME "Intel C++"
- # endif // # if defined(__VERSION__)
- #elif defined(_MSC_VER) // Microsoft VC++
- # if defined(_MSC_FULL_VER)
- # define COMPILER_NAME "Microsoft VC++ (" MACROVALUESTR(_MSC_FULL_VER) ")"
- # elif defined(_MSC_VER)
- # define COMPILER_NAME "Microsoft VC++ (" MACROVALUESTR(_MSC_VER) ")"
- # else
- # define COMPILER_NAME