基於SSE指令集的程序設計簡介 - 中國WEB開發者網絡 (http://www.webasp.net) -- 技術教程 (http://www.webasp.net/article/) --- 基於SSE指令集的程序設計簡介 (http://www.webasp.net/article/9/8386.htm) |
| -- 作者:未知 -- 發佈日期: 2004-03-23 |
| SSE技術簡介
Intel公司的單指令多數據流式擴展(SSE,Streaming SIMD Extensions)技術能夠有效增強CPU浮點運算的能力。Visual Studio .NET 2003提供了對SSE指令集的編程支持,從而允許用戶在C++代碼中不用編寫彙編代碼就可直接使用SSE指令的功能。MSDN中有關SSE技術的主題[1]有可能會使不熟悉使用SSE彙編指令編程的初學者感到困惑,但是在閱讀MSDN有關文檔的同時,參考一下Intel軟件說明書(Intel Software manuals)[2]會使你更清楚地理解使用SSE指令編程的要點。 SIMD(single-instruction, multiple-data)是一種使用單道指令處理多道數據流的CPU執行模式,即在一個CPU指令執行週期內用一道指令完成處理多個數據的操作。考慮一下下面這個任務:計算一個很長的浮點型數組中每一個元素的平方根。實現這個任務的算法可以這樣寫: for each f in array //對數組中的每一個元素 f = sqrt(f) //計算它的平方根 為了瞭解實現的細節,我們把上面的代碼這樣寫: for each f in array { 把f從內存加載到浮點寄存器 計算平方根 再把計算結果從寄存器中取出放入內存 } 具有Intel SSE指令集支持的處理器有8個128位的寄存器,每一個寄存器可以存放4個(32位)單精度的浮點數。SSE同時提供了一個指令集,其中的指令可以允許把浮點數加載到這些128位的寄存器之中,這些數就可以在這些寄存器中進行算術邏輯運算,然後把結果放回內存。採用SSE技術後,算法可以寫成下面的樣子: for each 4 members in array //對數組中的每4個元素 { 把數組中的這4個數加載到一個128位的SSE寄存器中 在一個CPU指令執行週期中完成計算這4個數的平方根的操作 把所得的4個結果取出寫入內存 } C++編程人員在使用SSE指令函數編程時不必關心這些128位的寄存器,你可以使用128位的數據類型「__m128」和一系列C++函數來實現這些算術和邏輯操作,而決定程序使用哪個SSE寄存器以及代碼優化是C++編譯器的任務。當需要對很長的浮點數數組中的元素進行處理的時候,SSE技術確實是一種很高效的方法。 SSE程序設計詳細介紹 包含的頭文件: 所有的SSE指令函數和__m128數據類型都在xmmintrin.h文件中定義: #include <xmmintrin.h> 因為程序中用到的SSE處理器指令是由編譯器決定,所以它並沒有相關的.lib庫文件。 數據分組(Data Alignment) 由SSE指令處理的每一個浮點數數組必須把其中需要處理的數每16個字節(128位二進制)分為一組。一個靜態數組(static array)可由__declspec(align(16))關鍵字聲明: __declspec(align(16)) float m_fArray[ARRAY_SIZE]; 動態數組(dynamic array)可由_aligned_malloc函數為其分配空間: m_fArray = (float*) _aligned_malloc(ARRAY_SIZE * sizeof(float), 16); 由_aligned_malloc函數分配空間的動態數組可以由_aligned_free函數釋放其佔用的空間: _aligned_free(m_fArray); __m128 數據類型 該數據類型的變量可用做SSE指令的操作數,它們不能被用戶指令直接存取。_m128類型的變量被自動分配為16個字節的字長。 CPU對SSE指令集的支持 如果你的CPU能夠具有了SSE指令集,你就可以使用Visual Studio .NET 2003提供的對SSE指令集支持的C++函數庫了,你可以查看MSDN中的一個Visual C++ CPUID的例子[4],它可以幫你檢測你的CPU是否支持SSE、MMX指令集或其它的CPU功能。 編程實例 以下講解了SSE技術在Visual Studio .NET 2003下的應用實例,你可以在http://www.codeproject.com/cpp/sseintro/SSE_src.zip下載示例程序壓縮包。該壓縮包中含有兩個項目,這兩個項目是基於微軟基本類庫(MFC)建立的Visual C++.NET項目,你也可以按照下面的講解建立這兩個項目。 SSETest 示例項目 SSETest項目是一個基於對話框的應用程序,它用到了三個浮點數組參與運算: fResult[i] = sqrt( fSource1[i]*fSource1[i] + fSource2[i]*fSource2[i] ) + 0.5 其中i = 0, 1, 2 ... ARRAY_SIZE-1 其中ARRAY_SIZE被定義為30000。數據源數組(Source數組)通過使用sin和cos函數給它賦值,我們用Kris Jearakul開發的瀑布狀圖表控件(Waterfall chart control)[3] 來顯示參與計算的源數組和結果數組。計算所需的時間(以毫秒ms為單位)在對話框中顯示出來。我們使用三種不同的途徑來完成計算: 純C++代碼; 使用SSE指令函數的C++代碼; 包含SSE彙編指令的代碼。 純C++代碼: void CSSETestDlg::ComputeArrayCPlusPlus( float* pArray1, // [輸入] 源數組1 float* pArray2, // [輸入] 源數組2 float* pResult, // [輸出] 用來存放結果的數組 int nSize) // [輸入] 數組的大小 { int i; float* pSource1 = pArray1; float* pSource2 = pArray2; float* pDest = pResult; for ( i = 0; i < nSize; i++ ) { *pDest = (float)sqrt((*pSource1) * (*pSource1) + (*pSource2) * (*pSource2)) + 0.5f; pSource1++; pSource2++; pDest++; } } 下面我們用具有SSE特性的C++代碼重寫上面這個函數。為了查詢使用SSE指令C++函數的方法,我參考了Intel軟件說明書(Intel Software manuals)中有關SSE彙編指令的說明,首先我是在第一卷的第九章找到的相關SSE指令,然後在第二卷找到了這些SSE指令的詳細說明,這些說明有一部分涉及了與其特性相關的C++函數。然後我通過這些SSE指令對應的C++函數查找了MSDN中與其相關的說明。搜索的結果見下表: 實現的功能 對應的SSE彙編指令 Visual C++.NET中的SSE函數 將4個32位浮點數放進一個128位的存儲單元。 movss 和 shufps _mm_set_ps1 將4對32位浮點數同時進行相乘操作。這4對32位浮點數來自兩個128位的存儲單元,再把計算結果(乘積)賦給一個128位的存儲單元。 mulps _mm_mul_ps 將4對32位浮點數同時進行相加操作。這4對32位浮點數來自兩個128位的存儲單元,再把計算結果(相加之和)賦給一個128位的存儲單元。 addps _mm_add_ps 對一個128位存儲單元中的4個32位浮點數同時進行求平方根操作。 sqrtps _mm_sqrt_ps 使用Visual C++.NET的 SSE指令函數的代碼: void CSSETestDlg::ComputeArrayCPlusPlusSSE( float* pArray1, // [輸入] 源數組1 float* pArray2, // [輸入] 源數組2 float* pResult, // [輸出] 用來存放結果的數組 int nSize) // [輸入] 數組的大小 { int nLoop = nSize/ 4; __m128 m1, m2, m3, m4; __m128* pSrc1 = (__m128*) pArray1; __m128* pSrc2 = (__m128*) pArray2; __m128* pDest = (__m128*) pResult; __m128 m0_5 = _mm_set_ps1(0.5f); // m0_5[0, 1, 2, 3] = 0.5 for ( int i = 0; i < nLoop; i++ ) { m1 = _mm_mul_ps(*pSrc1, *pSrc1); // m1 = *pSrc1 * *pSrc1 m2 = _mm_mul_ps(*pSrc2, *pSrc2); // m2 = *pSrc2 * *pSrc2 m3 = _mm_add_ps(m1, m2); // m3 = m1 + m2 m4 = _mm_sqrt_ps(m3); // m4 = sqrt(m3) *pDest = _mm_add_ps(m4, m0_5); // *pDest = m4 + 0.5 pSrc1++; pSrc2++; pDest++; } } 使用SSE彙編指令實現的C++函數代碼: void CSSETestDlg::ComputeArrayAssemblySSE( float* pArray1, // [輸入] 源數組1 float* pArray2, // [輸入] 源數組2 float* pResult, // [輸出] 用來存放結果的數組 int nSize) // [輸入] 數組的大小 { int nLoop = nSize/4; float f = 0.5f; _asm { movss xmm2, f // xmm2[0] = 0.5 shufps xmm2, xmm2, 0 // xmm2[1, 2, 3] = xmm2[0] mov esi, pArray1 // 輸入的源數組1的地址送往esi mov edx, pArray2 // 輸入的源數組2的地址送往edx mov edi, pResult // 輸出結果數組的地址保存在edi mov ecx, nLoop //循環次數送往ecx start_loop: movaps xmm0, [esi] // xmm0 = [esi] mulps xmm0, xmm0 // xmm0 = xmm0 * xmm0 movaps xmm1, [edx] // xmm1 = [edx] mulps xmm1, xmm1 // xmm1 = xmm1 * xmm1 addps xmm0, xmm1 // xmm0 = xmm0 + xmm1 sqrtps xmm0, xmm0 // xmm0 = sqrt(xmm0) addps xmm0, xmm2 // xmm0 = xmm1 + xmm2 movaps [edi], xmm0 // [edi] = xmm0 add esi, 16 // esi += 16 add edx, 16 // edx += 16 add edi, 16 // edi += 16 dec ecx // ecx-- jnz start_loop //如果不為0則轉向start_loop } } 最後,在我的計算機上運行計算測試的結果: 純C++代碼計算所用的時間是26 毫秒 使用SSE的C++ 函數計算所用的時間是 9 毫秒 包含SSE彙編指令的C++代碼計算所用的時間是 9 毫秒 以上的時間結果是在Release優化編譯後執行程序得出的。 SSESample 示例項目 SSESample項目是一個基於對話框的應用程序,其中它用下面的浮點數數組進行計算: fResult[i] = sqrt(fSource[i]*2.8) 其中i = 0, 1, 2 ... ARRAY_SIZE-1 這個程序同時計算了數組中的最大值和最小值。ARRAY_SIZE被定義為100000,數組中的計算結果在列表框中顯示出來。其中在我的機子上用下面三種方法計算所需的時間是: 純C++代碼計算 6 毫秒 使用SSE的C++ 函數計算 3 毫秒 使用SSE彙編指令計算 2 毫秒 大家看到,使用SSE彙編指令計算的結果會好一些,因為使用了效率增強了的SSX寄存器組。但是在通常情況下,使用SSE的C++ 函數計算會比彙編代碼計算的效率更高一些,因為C++編譯器的優化後的代碼有很高的運算效率,若要使彙編代碼比優化後的代碼運算效率更高,這通常是很難做到的。 純C++代碼: // 輸入: m_fInitialArray // 輸出: m_fResultArray, m_fMin, m_fMax void CSSESampleDlg::OnBnClickedButtonCplusplus() { m_fMin = FLT_MAX; m_fMax = FLT_MIN; int i; for ( i = 0; i < ARRAY_SIZE; i++ ) { m_fResultArray[i] = sqrt(m_fInitialArray[i] * 2.8f); if ( m_fResultArray[i] < m_fMin ) m_fMin = m_fResultArray[i]; if ( m_fResultArray[i] > m_fMax ) m_fMax = m_fResultArray[i]; } } 使用Visual C++.NET的 SSE指令函數的代碼: // 輸入: m_fInitialArray // 輸出: m_fResultArray, m_fMin, m_fMax void CSSESampleDlg::OnBnClickedButtonSseC() { __m128 coeff = _mm_set_ps1(2.8f); // coeff[0, 1, 2, 3] = 2.8 __m128 tmp; __m128 min128 = _mm_set_ps1(FLT_MAX); // min128[0, 1, 2, 3] = FLT_MAX __m128 max128 = _mm_set_ps1(FLT_MIN); // max128[0, 1, 2, 3] = FLT_MIN __m128* pSource = (__m128*) m_fInitialArray; __m128* pDest = (__m128*) m_fResultArray; for ( int i = 0; i < ARRAY_SIZE/4; i++ ) { tmp = _mm_mul_ps(*pSource, coeff); // tmp = *pSource * coeff *pDest = _mm_sqrt_ps(tmp); // *pDest = sqrt(tmp) min128 = _mm_min_ps(*pDest, min128); max128 = _mm_max_ps(*pDest, max128); pSource++; pDest++; } // 計算max128的最大值和min128的最小值 union u { __m128 m; float f[4]; } x; x.m = min128; m_fMin = min(x.f[0], min(x.f[1], min(x.f[2], x.f[3]))); x.m = max128; m_fMax = max(x.f[0], max(x.f[1], max(x.f[2], x.f[3]))); } 使用SSE彙編指令的C++函數代碼: // 輸入: m_fInitialArray // 輸出: m_fResultArray, m_fMin, m_fMax void CSSESampleDlg::OnBnClickedButtonSseAssembly() { float* pIn = m_fInitialArray; float* pOut = m_fResultArray; float f = 2.8f; float flt_min = FLT_MIN; float flt_max = FLT_MAX; __m128 min128; __m128 max128; // 使用以下的附加寄存器:xmm2、xmm3、xmm4: // xmm2 – 相乘係數 // xmm3 – 最小值 // xmm4 – 最大值 _asm { movss xmm2, f // xmm2[0] = 2.8 shufps xmm2, xmm2, 0 // xmm2[1, 2, 3] = xmm2[0] movss xmm3, flt_max // xmm3 = FLT_MAX shufps xmm3, xmm3, 0 // xmm3[1, 2, 3] = xmm3[0] movss xmm4, flt_min // xmm4 = FLT_MIN shufps xmm4, xmm4, 0 // xmm3[1, 2, 3] = xmm3[0] mov esi, pIn // 輸入數組的地址送往esi mov edi, pOut // 輸出數組的地址送往edi mov ecx, ARRAY_SIZE/4 // 循環計數器初始化 start_loop: movaps xmm1, [esi] // xmm1 = [esi] mulps xmm1, xmm2 // xmm1 = xmm1 * xmm2 sqrtps xmm1, xmm1 // xmm1 = sqrt(xmm1) movaps [edi], xmm1 // [edi] = xmm1 minps xmm3, xmm1 maxps xmm4, xmm1 add esi, 16 add edi, 16 dec ecx jnz start_loop movaps min128, xmm3 movaps max128, xmm4 } union u { __m128 m; float f[4]; } x; x.m = min128; m_fMin = min(x.f[0], min(x.f[1], min(x.f[2], x.f[3]))); x.m = max128; m_fMax = max(x.f[0], max(x.f[1], max(x.f[2], x.f[3]))); } 參考文檔: [1]MSDN, SSE技術主題: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/vclang/html/vcrefstreamingsimdextensions.asp [2]Intel軟件說明書(Intel Software manuals): http://developer.intel.com/design/archives/processors/mmx/index.htm [3] Kris Jearakul的瀑布狀圖表控件:http://www.codeguru.com/controls/Waterfall.shtml [4] Microsoft Visual C++ CPUID示例: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/vcsample/html/vcsamcpuiddeterminecpucapabilities.asp |
| webasp.net |