作者:zyl910。
本文面對對SSE等SIMD指令集有一定基礎的讀者,以雙精度浮點數組求和為例演示了如何跨平台使用SSE2、AVX指令集。支持vc、gcc編譯器,在Windows、Linux、Mac這三大平台上成功運行。
一、關鍵講解
前文(http://www.cnblogs.com/zyl910/archive/2012/10/22/simdsumfloat.html)演示了如何使用SSE、AVX指令集 處理 單精度浮點數組求和。現在對其進行改造,使用SSE2、AVX指令集 處理 雙精度浮點數組求和。因程序基本上差不多,文本就不詳細講解了,只說關鍵變化。
1.1 指令集簡介
先來看看支持雙精度浮點數的SIMD的指令集——
SSE指令集只支持單精度浮點運算,直到SSE2指令集才支持雙精度浮點數運算。SSE2定義了128位緊縮雙精度浮點類型,對應Intrinsic中的__m128d類型,它能一次能處理2個雙精度浮點數。
AVX指令集從一開始就支持單精度與雙精度浮點運算(但整數運算要等AVX2)。AVX定義了256位緊縮雙精度浮點類型,對應Intrinsic中的__m256d類型,它能一次能處理4個雙精度浮點數。
1.2 改造為 SSE2、AVX的雙精度浮點代碼
在使用Intrinsic函數時,將 SSE、AVX的單精度浮點代碼 改造為 SSE2、AVX的雙精度浮點代碼是很方便的。對比前文與本文的數組求和代碼,變更的地方有——
float | double | 備注 | ||||
指令 | Intrinsic | Asm | 指令 | Intrinsic | Asm | |
SSE | __m128 | XMMWORD | SSE2 | __m128d | XMMWORD | 類型 |
_mm_setzero_ps | XORPS | _mm_setzero_pd | XORPD | 賦0 | ||
_mm_load_ps | MOVAPS | _mm_load_pd | MOVAPD | 加載 | ||
_mm_add_ps | ADDPS | _mm_add_pd | ADDPD | 加法 | ||
AVX | __m256 | YMMWORD | AVX | __m256d | YMMWORD | 類型 |
_mm256_setzero_ps | VXORPS | _mm256_setzero_pd | VXORPD | 賦0 | ||
_mm256_load_ps | VMOVAPS | _mm256_load_pd | VMOVAPD | 加載 | ||
_mm256_add_ps | VADDPS | _mm256_add_pd | VADDPD | 加法 |
其次,還需要調整一下地址計算。對於C語言來說,將float指針改為double指針就能解決大多數地址計算問題了。然后再修正一下塊寬計算、改寫一下合並時的代碼,便大功告成了。
例如sumfloat_sse與sumdouble_sse——
// 單精度浮點數組求和_SSE版. float sumfloat_sse(const float* 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; // 加載. const float* p = pbuf; // SSE批量處理時所用的指針. const float* 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 = (const float*)&xfsSum; s = q[0] + q[1] + q[2] + q[3]; // 處理剩下的. for(i=0; i<cntRem; ++i) { s += p[i]; } return s; } // 雙精度浮點數組求和_SSE版. double sumdouble_sse(const double* pbuf, size_t cntbuf) { double s = 0; // 求和變量. size_t i; size_t nBlockWidth = 2; // 塊寬. SSE寄存器能一次處理2個double. size_t cntBlock = cntbuf / nBlockWidth; // 塊數. size_t cntRem = cntbuf % nBlockWidth; // 剩余數量. __m128d xfdSum = _mm_setzero_pd(); // 求和變量。[SSE2] XORPD. 賦初值0. __m128d xfdLoad; // 加載. const double* p = pbuf; // SSE批量處理時所用的指針. const double* q; // 將SSE變量上的多個數值合並時所用指針. // SSE批量處理. for(i=0; i<cntBlock; ++i) { xfdLoad = _mm_load_pd(p); // [SSE2] MOVAPD. 加載. xfdSum = _mm_add_pd(xfdSum, xfdLoad); // [SSE2] ADDPD. 雙精浮點緊縮加法. p += nBlockWidth; } // 合並. q = (const double*)&xfdSum; s = q[0] + q[1]; // 處理剩下的. for(i=0; i<cntRem; ++i) { s += p[i]; } return s; }
1.3 環境檢查
最后,別忘了檢查環境——
INTRIN_SSE2、INTRIN_AVX 宏是 zintrin.h 提供的,可用來在編譯時檢測編譯器是否支持SSE2、AVX指令集。
simd_sse_level、simd_avx_level函數是 ccpuid.h 提供的,可用來在運行時檢測當前系統環境是否支持SSE2、AVX指令集。
二、全部代碼
2.1 simdsumdouble.c
全部代碼——
#define __STDC_LIMIT_MACROS 1 // C99整數范圍常量. [純C程序可以不用, 而C++程序必須定義該宏.] #include <stdlib.h> #include <stdio.h> #include <time.h> #include "zintrin.h" #include "ccpuid.h" // 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 "Microsoft VC++" # endif // # if defined(_MSC_FULL_VER) #elif defined(__GNUC__) // GCC # if defined(__CYGWIN__) # define COMPILER_NAME "GCC(Cygmin) " __VERSION__ # elif defined(__MINGW32__) # define COMPILER_NAME "GCC(MinGW) " __VERSION__ # else # define COMPILER_NAME "GCC " __VERSION__ # endif // # if defined(_MSC_FULL_VER) #else # define COMPILER_NAME "Unknown Compiler" #endif // #if defined(__ICL) // Intel C++ ////////////////////////////////////////////////// // sumdouble: 雙精度浮點數組求和的函數 ////////////////////////////////////////////////// // 雙精度浮點數組求和_基本版. // // result: 返回數組求和結果. // pbuf: 數組的首地址. // cntbuf: 數組長度. double sumdouble_base(const double* pbuf, size_t cntbuf) { double s = 0; // 求和變量. size_t i; for(i=0; i<cntbuf; ++i) { s += pbuf[i]; } return s; } #ifdef INTRIN_SSE2 // 雙精度浮點數組求和_SSE版. double sumdouble_sse(const double* pbuf, size_t cntbuf) { double s = 0; // 求和變量. size_t i; size_t nBlockWidth = 2; // 塊寬. SSE寄存器能一次處理2個double. size_t cntBlock = cntbuf / nBlockWidth; // 塊數. size_t cntRem = cntbuf % nBlockWidth; // 剩余數量. __m128d xfdSum = _mm_setzero_pd(); // 求和變量。[SSE2] XORPD. 賦初值0. __m128d xfdLoad; // 加載. const double* p = pbuf; // SSE批量處理時所用的指針. const double* q; // 將SSE變量上的多個數值合並時所用指針. // SSE批量處理. for(i=0; i<cntBlock; ++i) { xfdLoad = _mm_load_pd(p); // [SSE2] MOVAPD. 加載. xfdSum = _mm_add_pd(xfdSum, xfdLoad); // [SSE2] ADDPD. 雙精浮點緊縮加法. p += nBlockWidth; } // 合並. q = (const double*)&xfdSum; s = q[0] + q[1]; // 處理剩下的. for(i=0; i<cntRem; ++i) { s += p[i]; } return s; } // 雙精度浮點數組求和_SSE四路循環展開版. double sumdouble_sse_4loop(const double* pbuf, size_t cntbuf) { double s = 0; // 返回值. size_t i; size_t nBlockWidth = 2*4; // 塊寬. SSE寄存器能一次處理2個double,然后循環展開4次. size_t cntBlock = cntbuf / nBlockWidth; // 塊數. size_t cntRem = cntbuf % nBlockWidth; // 剩余數量. __m128d xfdSum = _mm_setzero_pd(); // 求和變量。[SSE2] XORPD. 賦初值0. __m128d xfdSum1 = _mm_setzero_pd(); __m128d xfdSum2 = _mm_setzero_pd(); __m128d xfdSum3 = _mm_setzero_pd(); __m128d xfdLoad; // 加載. __m128d xfdLoad1; __m128d xfdLoad2; __m128d xfdLoad3; const double* p = pbuf; // SSE批量處理時所用的指針. const double* q; // 將SSE變量上的多個數值合並時所用指針. // SSE批量處理. for(i=0; i<cntBlock; ++i) { xfdLoad = _mm_load_pd(p); // [SSE2] MOVAPD. 加載. xfdLoad1 = _mm_load_pd(p+2); xfdLoad2 = _mm_load_pd(p+4); xfdLoad3 = _mm_load_pd(p+6); xfdSum = _mm_add_pd(xfdSum, xfdLoad); // [SSE2] ADDPD. 雙精浮點緊縮加法. xfdSum1 = _mm_add_pd(xfdSum1, xfdLoad1); xfdSum2 = _mm_add_pd(xfdSum2, xfdLoad2); xfdSum3 = _mm_add_pd(xfdSum3, xfdLoad3); p += nBlockWidth; } // 合並. xfdSum = _mm_add_pd(xfdSum, xfdSum1); // 兩兩合並(0~1). xfdSum2 = _mm_add_pd(xfdSum2, xfdSum3); // 兩兩合並(2~3). xfdSum = _mm_add_pd(xfdSum, xfdSum2); // 兩兩合並(0~3). q = (const double*)&xfdSum; s = q[0] + q[1]; // 處理剩下的. for(i=0; i<cntRem; ++i) { s += p[i]; } return s; } #endif // #ifdef INTRIN_SSE2 #ifdef INTRIN_AVX // 雙精度浮點數組求和_AVX版. double sumdouble_avx(const double* pbuf, size_t cntbuf) { double s = 0; // 求和變量. size_t i; size_t nBlockWidth = 4; // 塊寬. AVX寄存器能一次處理4個double. size_t cntBlock = cntbuf / nBlockWidth; // 塊數. size_t cntRem = cntbuf % nBlockWidth; // 剩余數量. __m256d yfdSum = _mm256_setzero_pd(); // 求和變量。[AVX] VXORPD. 賦初值0. __m256d yfdLoad; // 加載. const double* p = pbuf; // AVX批量處理時所用的指針. const double* q; // 將AVX變量上的多個數值合並時所用指針. // AVX批量處理. for(i=0; i<cntBlock; ++i) { yfdLoad = _mm256_load_pd(p); // [AVX] VMOVAPD. 加載. yfdSum = _mm256_add_pd(yfdSum, yfdLoad); // [AVX] VADDPD. 雙精浮點緊縮加法. p += nBlockWidth; } // 合並. q = (const double*)&yfdSum; s = q[0] + q[1] + q[2] + q[3]; // 處理剩下的. for(i=0; i<cntRem; ++i) { s += p[i]; } return s; } // 雙精度浮點數組求和_AVX四路循環展開版. double sumdouble_avx_4loop(const double* pbuf, size_t cntbuf) { double s = 0; // 求和變量. size_t i; size_t nBlockWidth = 4*4; // 塊寬. AVX寄存器能一次處理8個double,然后循環展開4次. size_t cntBlock = cntbuf / nBlockWidth; // 塊數. size_t cntRem = cntbuf % nBlockWidth; // 剩余數量. __m256d yfdSum = _mm256_setzero_pd(); // 求和變量。[AVX] VXORPD. 賦初值0. __m256d yfdSum1 = _mm256_setzero_pd(); __m256d yfdSum2 = _mm256_setzero_pd(); __m256d yfdSum3 = _mm256_setzero_pd(); __m256d yfdLoad; // 加載. __m256d yfdLoad1; __m256d yfdLoad2; __m256d yfdLoad3; const double* p = pbuf; // AVX批量處理時所用的指針. const double* q; // 將AVX變量上的多個數值合並時所用指針. // AVX批量處理. for(i=0; i<cntBlock; ++i) { yfdLoad = _mm256_load_pd(p); // [AVX] VMOVAPD. 加載. yfdLoad1 = _mm256_load_pd(p+4); yfdLoad2 = _mm256_load_pd(p+8); yfdLoad3 = _mm256_load_pd(p+12); yfdSum = _mm256_add_pd(yfdSum, yfdLoad); // [AVX] VADDPD. 雙精浮點緊縮加法. yfdSum1 = _mm256_add_pd(yfdSum1, yfdLoad1); yfdSum2 = _mm256_add_pd(yfdSum2, yfdLoad2); yfdSum3 = _mm256_add_pd(yfdSum3, yfdLoad3); p += nBlockWidth; } // 合並. yfdSum = _mm256_add_pd(yfdSum, yfdSum1); // 兩兩合並(0~1). yfdSum2 = _mm256_add_pd(yfdSum2, yfdSum3); // 兩兩合並(2~3). yfdSum = _mm256_add_pd(yfdSum, yfdSum2); // 兩兩合並(0~3). q = (const double*)&yfdSum; s = q[0] + q[1] + q[2] + q[3]; // 處理剩下的. for(i=0; i<cntRem; ++i) { s += p[i]; } return s; } #endif // #ifdef INTRIN_AVX ////////////////////////////////////////////////// // main ////////////////////////////////////////////////// // 變量對齊. #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 2048 // = 32KB{L1 Cache} / (2 * sizeof(double)) ATTR_ALIGN(32) double buf[BUFSIZE]; // 測試時的函數類型 typedef double (*TESTPROC)(const double* pbuf, size_t cntbuf); // 進行測試 void runTest(const char* szname, TESTPROC proc) { const int testloop = 4000; // 重復運算幾次延長時間,避免計時精度問題. const clock_t TIMEOUT = CLOCKS_PER_SEC/2; // 最短測試時間. int i,j,k; clock_t tm0, dt; // 存儲時間. double mps; // M/s. double mps_good = 0; // 最佳M/s. 因線程切換會導致的數值波動, 於是選取最佳值. volatile double 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); } int main(int argc, char* argv[]) { char szBuf[64]; int i; printf("simdsumdouble v1.00 (%dbit)\n", INTRIN_WORDSIZE); printf("Compiler: %s\n", COMPILER_NAME); cpu_getbrand(szBuf); printf("CPU:\t%s\n", szBuf); printf("\n"); // init buf srand( (unsigned)time( NULL ) ); for (i = 0; i < BUFSIZE; i++) buf[i] = (double)(rand() & 0x7fff); // 使用&0x7fff是為了讓求和后的數值在一定范圍內,便於觀察結果是否正確. // test runTest("sumdouble_base", sumdouble_base); // 雙精度浮點數組求和_基本版. #ifdef INTRIN_SSE2 if (simd_sse_level(NULL) >= SIMD_SSE_2) { runTest("sumdouble_sse", sumdouble_sse); // 雙精度浮點數組求和_SSE版. runTest("sumdouble_sse_4loop", sumdouble_sse_4loop); // 雙精度浮點數組求和_SSE四路循環展開版. } #endif // #ifdef INTRIN_SSE2 #ifdef INTRIN_AVX if (simd_avx_level(NULL) >= SIMD_AVX_1) { runTest("sumdouble_avx", sumdouble_avx); // 雙精度浮點數組求和_SSE版. runTest("sumdouble_avx_4loop", sumdouble_avx_4loop); // 雙精度浮點數組求和_SSE四路循環展開版. } #endif // #ifdef INTRIN_AVX return 0; }
2.2 makefile
全部代碼——
# flags CC = g++ CFS = -Wall -msse2 # args RELEASE =0 BITS = CFLAGS = # [args] 生成模式. 0代表debug模式, 1代表release模式. make RELEASE=1. ifeq ($(RELEASE),0) # debug CFS += -g else # release CFS += -O3 -DNDEBUG //CFS += -O3 -g -DNDEBUG endif # [args] 程序位數. 32代表32位程序, 64代表64位程序, 其他默認. make BITS=32. ifeq ($(BITS),32) CFS += -m32 else ifeq ($(BITS),64) CFS += -m64 else endif endif # [args] 使用 CFLAGS 添加新的參數. make CFLAGS="-mavx". CFS += $(CFLAGS) .PHONY : all clean # files TARGETS = simdsumdouble OBJS = simdsumdouble.o all : $(TARGETS) simdsumdouble : $(OBJS) $(CC) $(CFS) -o $@ $^ simdsumdouble.o : simdsumdouble.c zintrin.h ccpuid.h $(CC) $(CFS) -c $< clean : rm -f $(OBJS) $(TARGETS) $(addsuffix .exe,$(TARGETS))
三、編譯測試
3.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版。
3.2 測試
因虛擬機上的有效率損失,於是僅在真實系統上進行測試。
系統環境——
CPU:Intel(R) Core(TM) i3-2310M CPU @ 2.10GHz
操作系統:Windows 7 SP1 x64版
然后分別運行VC與GCC編譯的Release版可執行文件,即以下4個程序——
exe\simdsumdouble_vc32.exe:VC2010 SP1 編譯的32位程序,/O2 /arch:SSE2。
exe\simdsumdouble_vc64.exe:VC2010 SP1 編譯的64位程序,/O2 /arch:AVX。
exe\simdsumdouble_gcc32.exe:GCC 4.7.1(TDM-GCC(MinGW-w64)) 編譯的32位程序,-O3 -mavx。
exe\simdsumdouble_gcc64.exe:GCC 4.7.1(TDM-GCC(MinGW-w64)) 編譯的64位程序,-O3 -mavx。
測試結果(使用cmdarg_ui)——
參考文獻——
《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
《[C] 跨平台使用Intrinsic函數范例1——使用SSE、AVX指令集 處理 單精度浮點數組求和(支持vc、gcc,兼容Windows、Linux、Mac)》. http://www.cnblogs.com/zyl910/archive/2012/10/22/simdsumfloat.html