天天看點

[C] 跨平台使用Intrinsic函數範例1——使用SSE、AVX指令集 處理 單精度浮點數組求和(支援vc、gcc,相容Windows、Linux、Mac) 一、問題背景 二、範例講解 三、全部代碼 四、編譯測試

本文面對對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。而很多時候我們為了相容舊代碼,不得不使用VC6等老版本的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 事先準備

  為了解決上面提到的問題,我編寫了三個子產品——

stdint:智能支援C99的stdint.h,解決整數類型問題。最新版的位址是 http://www.cnblogs.com/zyl910/archive/2012/08/08/c99int.html 。

zintrin:在編譯時檢測Intrinsic函數集支援性,并自動引入相關頭檔案、修正細節問題。最新版的位址是 http://www.cnblogs.com/zyl910/archive/2012/10/01/zintrin_v101.html 。

ccpuid:在編譯時檢測指令集的支援性。最新版的位址是 http://www.cnblogs.com/zyl910/archive/2012/10/13/ccpuid_v103.html 。

  這三個子產品的純C版就是一個頭檔案,用起來很友善,将它們放在項目中,直接#include就行了。例如——

[cpp]  view plain  copy

  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語言編寫一個基本的單精度浮點數組求和函數——

[cpp]  view plain  copy

  1. // 單精度浮點數組求和_基本版.  
  2. //  
  3. // result: 傳回數組求和結果.  
  4. // pbuf: 數組的首位址.  
  5. // cntbuf: 數組長度.  
  6. float sumfloat_base(const float* 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宏進行條件編譯。

  代碼如下——

[cpp]  view plain  copy

  1. #ifdef INTRIN_SSE  
  2. // 單精度浮點數組求和_SSE版.  
  3. float sumfloat_sse(const float* 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.     const float* p = pbuf;  // SSE批量處理時所用的指針.  
  13.     const float* 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 = (const float*)&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四路循環展開版

  循環展開可以降低循環開銷,提高指令級并行性能。

  一般來說,四路循環展開就差不多夠了。我們可以很友善的将上一節的代碼改造為四路循環展開版——

[cpp]  view plain  copy

  1. // 單精度浮點數組求和_SSE四路循環展開版.  
  2. float sumfloat_sse_4loop(const float* 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.     const float* p = pbuf;  // SSE批量處理時所用的指針.  
  18.     const float* 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 = (const float*)&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宏進行條件編譯。

  代碼如下——

[cpp]  view plain  copy

  1. #ifdef INTRIN_AVX  
  2. // 單精度浮點數組求和_AVX版.  
  3. float sumfloat_avx(const float* 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.     const float* p = pbuf;  // AVX批量處理時所用的指針.  
  13.     const float* 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 = (const float*)&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”——

[C] 跨平台使用Intrinsic函數範例1——使用SSE、AVX指令集 處理 單精度浮點數組求和(支援vc、gcc,相容Windows、Linux、Mac) 一、問題背景 二、範例講解 三、全部代碼 四、編譯測試

詳見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四路循環展開版——

[cpp]  view plain  copy

  1. // 單精度浮點數組求和_AVX四路循環展開版.  
  2. float sumfloat_avx_4loop(const float* 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.     const float* p = pbuf;  // AVX批量處理時所用的指針.  
  18.     const float* 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 = (const float*)&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宏來統一處理變量的記憶體對齊問題。

  該數組定義如下——

[cpp]  view plain  copy

  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函數指針進行測試的函數——

[cpp]  view plain  copy

  1. // 進行測試  
  2. void runTest(const char* szname, TESTPROC proc)  
  3. {  
  4.     const int testloop = 4000;  // 重複運算幾次延長時間,避免計時精度問題.  
  5.     const clock_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.     volatile float 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數組進行初始化,将數組元素賦随機值——

[cpp]  view plain  copy

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

  然後可以開始測試了——

[cpp]  view plain  copy

  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函數中加上——

[cpp]  view plain  copy

  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 是一個用來獲得編譯器名稱的宏,它的詳細定義是——

[cpp]  view plain  copy

  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   "Microsoft VC++"  
  19. #  endif    // #  if defined(_MSC_FULL_VER)  
  20. #elif defined(__GNUC__) // GCC  
  21. #  if defined(__CYGWIN__)  
  22. #    define COMPILER_NAME   "GCC(Cygmin) " __VERSION__  
  23. #  elif defined(__MINGW32__)  
  24. #    define COMPILER_NAME   "GCC(MinGW) " __VERSION__  
  25. #  else  
  26. #    define COMPILER_NAME   "GCC " __VERSION__  
  27. #  endif    // #  if defined(_MSC_FULL_VER)  
  28. #else  
  29. #  define COMPILER_NAME "Unknown Compiler"  
  30. #endif  // #if defined(__ICL)   // Intel C++  

三、全部代碼

3.1 simdsumfloat.c

  全部代碼——

[cpp]  view plain  copy

  1. #define __STDC_LIMIT_MACROS 1   // C99整數範圍常量. [純C程式可以不用, 而C++程式必須定義該宏.]  
  2. #include <stdlib.h>  
  3. #include <stdio.h>  
  4. #include <time.h>  
  5. #include "zintrin.h"  
  6. #include "ccpuid.h"  
  7. // Compiler name  
  8. #define MACTOSTR(x) #x  
  9. #define MACROVALUESTR(x)    MACTOSTR(x)  
  10. #if defined(__ICL)  // Intel C++  
  11. #  if defined(__VERSION__)  
  12. #    define COMPILER_NAME   "Intel C++ " __VERSION__  
  13. #  elif defined(__INTEL_COMPILER_BUILD_DATE)  
  14. #    define COMPILER_NAME   "Intel C++ (" MACROVALUESTR(__INTEL_COMPILER_BUILD_DATE) ")"  
  15. #  else  
  16. #    define COMPILER_NAME   "Intel C++"  
  17. #  endif    // #  if defined(__VERSION__)  
  18. #elif defined(_MSC_VER) // Microsoft VC++  
  19. #  if defined(_MSC_FULL_VER)  
  20. #    define COMPILER_NAME   "Microsoft VC++ (" MACROVALUESTR(_MSC_FULL_VER) ")"  
  21. #  elif defined(_MSC_VER)  
  22. #    define COMPILER_NAME   "Microsoft VC++ (" MACROVALUESTR(_MSC_VER) ")"  
  23. #  else  
  24. #    define COMPILER_NAME   "Microsoft VC++"  
  25. #  endif    // #  if defined(_MSC_FULL_VER)  
  26. #elif defined(__GNUC__) // GCC  
  27. #  if defined(__CYGWIN__)  
  28. #    define COMPILER_NAME   "GCC(Cygmin) " __VERSION__  
  29. #  elif defined(__MINGW32__)  
  30. #    define COMPILER_NAME   "GCC(MinGW) " __VERSION__  
  31. #  else  
  32. #    define COMPILER_NAME   "GCC " __VERSION__  
  33. #  endif    // #  if defined(_MSC_FULL_VER)  
  34. #else  
  35. #  define COMPILER_NAME "Unknown Compiler"  
  36. #endif  // #if defined(__ICL)   // Intel C++  
  37. //  
  38. // sumfloat: 單精度浮點數組求和的函數  
  39. //  
  40. // 單精度浮點數組求和_基本版.  
  41. //  
  42. // result: 傳回數組求和結果.  
  43. // pbuf: 數組的首位址.  
  44. // cntbuf: 數組長度.  
  45. float sumfloat_base(const float* pbuf, size_t cntbuf)  
  46. {  
  47.     float s = 0;    // 求和變量.  
  48.     size_t i;  
  49.     for(i=0; i<cntbuf; ++i)  
  50.     {  
  51.         s += pbuf[i];  
  52.     }  
  53.     return s;  
  54. }  
  55. #ifdef INTRIN_SSE  
  56. // 單精度浮點數組求和_SSE版.  
  57. float sumfloat_sse(const float* pbuf, size_t cntbuf)  
  58. {  
  59.     float s = 0;    // 求和變量.  
  60.     size_t i;  
  61.     size_t nBlockWidth = 4; // 塊寬. SSE寄存器能一次處理4個float.  
  62.     size_t cntBlock = cntbuf / nBlockWidth; // 塊數.  
  63.     size_t cntRem = cntbuf % nBlockWidth;   // 剩餘數量.  
  64.     __m128 xfsSum = _mm_setzero_ps();   // 求和變量。[SSE] 賦初值0  
  65.     __m128 xfsLoad; // 加載.  
  66.     const float* p = pbuf;  // SSE批量處理時所用的指針.  
  67.     const float* q; // 将SSE變量上的多個數值合并時所用指針.  
  68.     // SSE批量處理.  
  69.     for(i=0; i<cntBlock; ++i)  
  70.     {  
  71.         xfsLoad = _mm_load_ps(p);   // [SSE] 加載  
  72.         xfsSum = _mm_add_ps(xfsSum, xfsLoad);   // [SSE] 單精浮點緊縮加法  
  73.         p += nBlockWidth;  
  74.     }  
  75.     // 合并.  
  76.     q = (const float*)&xfsSum;  
  77.     s = q[0] + q[1] + q[2] + q[3];  
  78.     // 處理剩下的.  
  79.     for(i=0; i<cntRem; ++i)  
  80.     {  
  81.         s += p[i];  
  82.     }  
  83.     return s;  
  84. }  
  85. // 單精度浮點數組求和_SSE四路循環展開版.  
  86. float sumfloat_sse_4loop(const float* pbuf, size_t cntbuf)  
  87. {  
  88.     float s = 0;    // 傳回值.  
  89.     size_t i;  
  90.     size_t nBlockWidth = 4*4;   // 塊寬. SSE寄存器能一次處理4個float,然後循環展開4次.  
  91.     size_t cntBlock = cntbuf / nBlockWidth; // 塊數.  
  92.     size_t cntRem = cntbuf % nBlockWidth;   // 剩餘數量.  
  93.     __m128 xfsSum = _mm_setzero_ps();   // 求和變量。[SSE] 賦初值0  
  94.     __m128 xfsSum1 = _mm_setzero_ps();  
  95.     __m128 xfsSum2 = _mm_setzero_ps();  
  96.     __m128 xfsSum3 = _mm_setzero_ps();  
  97.     __m128 xfsLoad; // 加載.  
  98.     __m128 xfsLoad1;  
  99.     __m128 xfsLoad2;  
  100.     __m128 xfsLoad3;  
  101.     const float* p = pbuf;  // SSE批量處理時所用的指針.  
  102.     const float* q; // 将SSE變量上的多個數值合并時所用指針.  
  103.     // SSE批量處理.  
  104.     for(i=0; i<cntBlock; ++i)  
  105.     {  
  106.         xfsLoad = _mm_load_ps(p);   // [SSE] 加載.  
  107.         xfsLoad1 = _mm_load_ps(p+4);  
  108.         xfsLoad2 = _mm_load_ps(p+8);  
  109.         xfsLoad3 = _mm_load_ps(p+12);  
  110.         xfsSum = _mm_add_ps(xfsSum, xfsLoad);   // [SSE] 單精浮點緊縮加法  
  111.         xfsSum1 = _mm_add_ps(xfsSum1, xfsLoad1);  
  112.         xfsSum2 = _mm_add_ps(xfsSum2, xfsLoad2);  
  113.         xfsSum3 = _mm_add_ps(xfsSum3, xfsLoad3);  
  114.         p += nBlockWidth;  
  115.     }  
  116.     // 合并.  
  117.     xfsSum = _mm_add_ps(xfsSum, xfsSum1);   // 兩兩合并(0~1).  
  118.     xfsSum2 = _mm_add_ps(xfsSum2, xfsSum3); // 兩兩合并(2~3).  
  119.     xfsSum = _mm_add_ps(xfsSum, xfsSum2);   // 兩兩合并(0~3).  
  120.     q = (const float*)&xfsSum;  
  121.     s = q[0] + q[1] + q[2] + q[3];  
  122.     // 處理剩下的.  
  123.     for(i=0; i<cntRem; ++i)  
  124.     {  
  125.         s += p[i];  
  126.     }  
  127.     return s;  
  128. }  
  129. #endif  // #ifdef INTRIN_SSE  
  130. #ifdef INTRIN_AVX  
  131. // 單精度浮點數組求和_AVX版.  
  132. float sumfloat_avx(const float* pbuf, size_t cntbuf)  
  133. {  
  134.     float s = 0;    // 求和變量.  
  135.     size_t i;  
  136.     size_t nBlockWidth = 8; // 塊寬. AVX寄存器能一次處理8個float.  
  137.     size_t cntBlock = cntbuf / nBlockWidth; // 塊數.  
  138.     size_t cntRem = cntbuf % nBlockWidth;   // 剩餘數量.  
  139.     __m256 yfsSum = _mm256_setzero_ps();    // 求和變量。[AVX] 賦初值0  
  140.     __m256 yfsLoad; // 加載.  
  141.     const float* p = pbuf;  // AVX批量處理時所用的指針.  
  142.     const float* q; // 将AVX變量上的多個數值合并時所用指針.  
  143.     // AVX批量處理.  
  144.     for(i=0; i<cntBlock; ++i)  
  145.     {  
  146.         yfsLoad = _mm256_load_ps(p);    // [AVX] 加載  
  147.         yfsSum = _mm256_add_ps(yfsSum, yfsLoad);    // [AVX] 單精浮點緊縮加法  
  148.         p += nBlockWidth;  
  149.     }  
  150.     // 合并.  
  151.     q = (const float*)&yfsSum;  
  152.     s = q[0] + q[1] + q[2] + q[3] + q[4] + q[5] + q[6] + q[7];  
  153.     // 處理剩下的.  
  154.     for(i=0; i<cntRem; ++i)  
  155.     {  
  156.         s += p[i];  
  157.     }  
  158.     return s;  
  159. }  
  160. // 單精度浮點數組求和_AVX四路循環展開版.  
  161. float sumfloat_avx_4loop(const float* pbuf, size_t cntbuf)  
  162. {  
  163.     float s = 0;    // 求和變量.  
  164.     size_t i;  
  165.     size_t nBlockWidth = 8*4;   // 塊寬. AVX寄存器能一次處理8個float,然後循環展開4次.  
  166.     size_t cntBlock = cntbuf / nBlockWidth; // 塊數.  
  167.     size_t cntRem = cntbuf % nBlockWidth;   // 剩餘數量.  
  168.     __m256 yfsSum = _mm256_setzero_ps();    // 求和變量。[AVX] 賦初值0  
  169.     __m256 yfsSum1 = _mm256_setzero_ps();  
  170.     __m256 yfsSum2 = _mm256_setzero_ps();  
  171.     __m256 yfsSum3 = _mm256_setzero_ps();  
  172.     __m256 yfsLoad; // 加載.  
  173.     __m256 yfsLoad1;  
  174.     __m256 yfsLoad2;  
  175.     __m256 yfsLoad3;  
  176.     const float* p = pbuf;  // AVX批量處理時所用的指針.  
  177.     const float* q; // 将AVX變量上的多個數值合并時所用指針.  
  178.     // AVX批量處理.  
  179.     for(i=0; i<cntBlock; ++i)  
  180.     {  
  181.         yfsLoad = _mm256_load_ps(p);    // [AVX] 加載.  
  182.         yfsLoad1 = _mm256_load_ps(p+8);  
  183.         yfsLoad2 = _mm256_load_ps(p+16);  
  184.         yfsLoad3 = _mm256_load_ps(p+24);  
  185.         yfsSum = _mm256_add_ps(yfsSum, yfsLoad);    // [AVX] 單精浮點緊縮加法  
  186.         yfsSum1 = _mm256_add_ps(yfsSum1, yfsLoad1);  
  187.         yfsSum2 = _mm256_add_ps(yfsSum2, yfsLoad2);  
  188.         yfsSum3 = _mm256_add_ps(yfsSum3, yfsLoad3);  
  189.         p += nBlockWidth;  
  190.     }  
  191.     // 合并.  
  192.     yfsSum = _mm256_add_ps(yfsSum, yfsSum1);    // 兩兩合并(0~1).  
  193.     yfsSum2 = _mm256_add_ps(yfsSum2, yfsSum3);  // 兩兩合并(2~3).  
  194.     yfsSum = _mm256_add_ps(yfsSum, yfsSum2);    // 兩兩合并(0~3).  
  195.     q = (const float*)&yfsSum;  
  196.     s = q[0] + q[1] + q[2] + q[3] + q[4] + q[5] + q[6] + q[7];  
  197.     // 處理剩下的.  
  198.     for(i=0; i<cntRem; ++i)  
  199.     {  
  200.         s += p[i];  
  201.     }  
  202.     return s;  
  203. }  
  204. #endif  // #ifdef INTRIN_AVX  
  205. //  
  206. // main  
  207. //  
  208. // 變量對齊.  
  209. #ifndef ATTR_ALIGN  
  210. #  if defined(__GNUC__) // GCC  
  211. #    define ATTR_ALIGN(n)   __attribute__((aligned(n)))  
  212. #  else // 否則使用VC格式.  
  213. #    define ATTR_ALIGN(n)   __declspec(align(n))  
  214. #  endif  
  215. #endif  // #ifndef ATTR_ALIGN  
  216. #define BUFSIZE 4096    // = 32KB{L1 Cache} / (2 * sizeof(float))  
  217. ATTR_ALIGN(32) float buf[BUFSIZE];  
  218. // 測試時的函數類型  
  219. typedef float (*TESTPROC)(const float* pbuf, size_t cntbuf);  
  220. // 進行測試  
  221. void runTest(const char* szname, TESTPROC proc)  
  222. {  
  223.     const int testloop = 4000;  // 重複運算幾次延長時間,避免計時精度問題.  
  224.     const clock_t TIMEOUT = CLOCKS_PER_SEC/2;   // 最短測試時間.  
  225.     int i,j,k;  
  226.     clock_t tm0, dt;    // 存儲時間.  
  227.     double mps; // M/s.  
  228.     double mps_good = 0;    // 最佳M/s. 因線程切換會導緻的數值波動, 于是選取最佳值.  
  229.     volatile float n=0; // 避免内循環被優化.  
  230.     for(i=1; i<=3; ++i)  // 多次測試.  
  231.     {  
  232.         tm0 = clock();  
  233.         // main  
  234.         k=0;  
  235.         do  
  236.         {  
  237.             for(j=1; j<=testloop; ++j)   // 重複運算幾次延長時間,避免計時開銷帶來的影響.  
  238.             {  
  239.                 n = proc(buf, BUFSIZE); // 避免内循環被編譯優化消掉.  
  240.             }  
  241.             ++k;  
  242.             dt = clock() - tm0;  
  243.         }while(dt<TIMEOUT);  
  244.         // show  
  245.         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 .  
  246.         if (mps_good<mps)    mps_good=mps;   // 選取最佳值.  
  247.         //printf("%s:\t%.0f M/s\t//%f\n", szname, mps, n);  
  248.     }  
  249.     printf("%s:\t%.0f M/s\t//%f\n", szname, mps_good, n);  
  250. }  
  251. int main(int argc, char* argv[])  
  252. {  
  253.     char szBuf[64];  
  254.     int i;  
  255.     printf("simdsumfloat v1.00 (%dbit)\n", INTRIN_WORDSIZE);  
  256.     printf("Compiler: %s\n", COMPILER_NAME);  
  257.     cpu_getbrand(szBuf);  
  258.     printf("CPU:\t%s\n", szBuf);  
  259.     printf("\n");  
  260.     // init buf  
  261.     srand( (unsigned)time( NULL ) );  
  262.     for (i = 0; i < BUFSIZE; i++) buf[i] = (float)(rand() & 0x3f);   // 使用&0x3f是為了讓求和後的數值不會超過float類型的有效位數,便于觀察結果是否正确.  
  263.     // test  
  264.     runTest("sumfloat_base", sumfloat_base);    // 單精度浮點數組求和_基本版.  
  265. #ifdef INTRIN_SSE  
  266.     if (simd_sse_level(NULL) >= SIMD_SSE_1)  
  267.     {  
  268.         runTest("sumfloat_sse", sumfloat_sse);  // 單精度浮點數組求和_SSE版.  
  269.         runTest("sumfloat_sse_4loop", sumfloat_sse_4loop);  // 單精度浮點數組求和_SSE四路循環展開版.  
  270.     }  
  271. #endif  // #ifdef INTRIN_SSE  
  272. #ifdef INTRIN_AVX  
  273.     if (simd_avx_level(NULL) >= SIMD_AVX_1)  
  274.     {  
  275.         runTest("sumfloat_avx", sumfloat_avx);  // 單精度浮點數組求和_AVX版.  
  276.         runTest("sumfloat_avx_4loop", sumfloat_avx_4loop);  // 單精度浮點數組求和_AVX四路循環展開版.  
  277.     }  
  278. #endif  // #ifdef INTRIN_AVX  
  279.     return 0;  
  280. }  

3.2 makefile

  全部代碼——

[plain]  view plain  copy

  1. # flags  
  2. CC = g++  
  3. CFS = -Wall -msse  
  4. # args  
  5. RELEASE =0  
  6. BITS =  
  7. CFLAGS =  
  8. # [args] 生成模式. 0代表debug模式, 1代表release模式. make RELEASE=1.  
  9. ifeq ($(RELEASE),0)  
  10.     # debug  
  11.     CFS += -g  
  12. else  
  13.     # release  
  14.     CFS += -O3 -DNDEBUG  
  15.     //CFS += -O3 -g -DNDEBUG  
  16. endif  
  17. # [args] 程式位數. 32代表32位程式, 64代表64位程式, 其他預設. make BITS=32.  
  18. ifeq ($(BITS),32)  
  19.     CFS += -m32  
  20. else  
  21.     ifeq ($(BITS),64)  
  22.         CFS += -m64  
  23.     else  
  24.     endif  
  25. endif  
  26. # [args] 使用 CFLAGS 添加新的參數. make CFLAGS="-mavx".  
  27. CFS += $(CFLAGS)  
  28. .PHONY : all clean  
  29. # files  
  30. TARGETS = simdsumfloat  
  31. OBJS = simdsumfloat.o  
  32. all : $(TARGETS)  
  33. simdsumfloat : $(OBJS)  
  34.     $(CC) $(CFS) -o $@ $^  
  35. simdsumfloat.o : simdsumfloat.c zintrin.h ccpuid.h  
  36.     $(CC) $(CFS) -c $<  
  37. clean :  
  38.     rm -f $(OBJS) $(TARGETS) $(addsuffix .exe,$(TARGETS))  

四、編譯測試

4.1 編譯

  在以下編譯器中成功編譯——

VC6:x86版。

VC2003:x86版。

VC2005:x86版。

VC2010:x86版、x64版。

GCC 4.7.0(Fedora 17 x64):x86版、x64版。

GCC 4.6.2(MinGW(20120426)):x86版。

GCC 4.7.1(TDM-GCC(MinGW-w64)):x86版、x64版。

llvm-gcc-4.2(Mac OS X Lion 10.7.4, Xcode 4.4.1):x86版、x64版。

[C] 跨平台使用Intrinsic函數範例1——使用SSE、AVX指令集 處理 單精度浮點數組求和(支援vc、gcc,相容Windows、Linux、Mac) 一、問題背景 二、範例講解 三、全部代碼 四、編譯測試
[C] 跨平台使用Intrinsic函數範例1——使用SSE、AVX指令集 處理 單精度浮點數組求和(支援vc、gcc,相容Windows、Linux、Mac) 一、問題背景 二、範例講解 三、全部代碼 四、編譯測試
[C] 跨平台使用Intrinsic函數範例1——使用SSE、AVX指令集 處理 單精度浮點數組求和(支援vc、gcc,相容Windows、Linux、Mac) 一、問題背景 二、範例講解 三、全部代碼 四、編譯測試

4.2 測試

  因虛拟機上的有效率損失,于是僅在真實系統上進行測試。

  系統環境——

CPU:Intel(R) Core(TM) i3-2310M CPU @ 2.10GHz

作業系統:Windows 7 SP1 x64版

  然後分别運作VC與GCC編譯的Release版可執行檔案,即以下4個程式——

exe\simdsumfloat_vc32.exe:VC2010 SP1 編譯的32位程式,/O2 /arch:SSE2。

exe\simdsumfloat_vc64.exe:VC2010 SP1 編譯的64位程式,/O2 /arch:AVX。

exe\simdsumfloat_gcc32.exe:GCC 4.7.1(TDM-GCC(MinGW-w64)) 編譯的32位程式,-O3 -mavx。

exe\simdsumfloat_gcc64.exe:GCC 4.7.1(TDM-GCC(MinGW-w64)) 編譯的64位程式,-O3 -mavx。

  測試結果(使用cmdarg_ui)——

[C] 跨平台使用Intrinsic函數範例1——使用SSE、AVX指令集 處理 單精度浮點數組求和(支援vc、gcc,相容Windows、Linux、Mac) 一、問題背景 二、範例講解 三、全部代碼 四、編譯測試

參考文獻——

《Intel® 64 and IA-32 Architectures Software Developer’s Manual Combined Volumes:1, 2A, 2B, 2C, 3A, 3B, and 3C》044US. August 2012.http://www.intel.com/content/www/us/en/processors/architectures-software-developer-manuals.html

《Intel® Architecture Instruction Set Extensions Programming Reference》014. AUGUST 2012.http://software.intel.com/en-us/avx/

《AMD64 Architecture Programmer’s Manual Volume 4: 128-Bit and 256-Bit Media Instructions》. December 2011.http://developer.amd.com/documentation/guides/Pages/default.aspx#manuals

《[C] 讓VC、BCB支援C99的整數類型(stdint.h、inttypes.h)(相容GCC)》. http://www.cnblogs.com/zyl910/archive/2012/08/08/c99int.html

《[C] zintrin.h: 智能引入intrinsic函數 V1.01版。改進對Mac OS X的支援,增加INTRIN_WORDSIZE宏》. http://www.cnblogs.com/zyl910/archive/2012/10/01/zintrin_v101.html

《[C/C++] ccpuid:CPUID資訊子產品 V1.03版,改進mmx/sse指令可用性檢查(使用signal、setjmp,支援純C)、修正AVX檢查Bug》.http://www.cnblogs.com/zyl910/archive/2012/10/13/ccpuid_v103.html

《[x86]SIMD指令集發展曆程表(MMX、SSE、AVX等)》. http://www.cnblogs.com/zyl910/archive/2012/02/26/x86_simd_table.html

《SIMD(MMX/SSE/AVX)變量命名規範心得》. http://www.cnblogs.com/zyl910/archive/2012/04/23/simd_var_name.html

《GCC 64位程式的makefile條件編譯心得——32位版與64位版、debug版與release版(相容MinGW、TDM-GCC)》. http://www.cnblogs.com/zyl910/archive/2012/08/14/gcc64_make.html

《[C#] cmdarg_ui:“簡單參數指令行程式”的通用圖形界面》.  http://www.cnblogs.com/zyl910/archive/2012/06/19/cmdarg_ui.html

源碼下載下傳—— 

http://files.cnblogs.com/zyl910/simdsumfloat.rar

文章轉自:http://blog.csdn.net/zyl910/article/details/8100744,感謝作者分享。

繼續閱讀