1. 程式人生 > >SSE指令集入門

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++

  1. void CSSETestDlg::ComputeArrayCPlusPlus(  
  2.           float* pArray1,                   // [in] first source array
  3.           float* pArray2,                   
    // [in] second source array
  4.           float* pResult,                   // [out] result array
  5.           int nSize)                        // [in] size of all arrays
  6. {  
  7.     int i;  
  8.     float* pSource1 = pArray1;  
  9.     float* pSource2 = pArray2;  
  10.     float* pDest = pResult;  
  11.     for ( i = 0; i < nSize; i++ )  
  12.     {  
  13.         *pDest = (float)sqrt((*pSource1) * (*pSource1) + (*pSource2)  
  14.                  * (*pSource2)) + 0.5f;  
  15.         pSource1++;  
  16.         pSource2++;  
  17.         pDest++;  
  18.     }  
  19. }  


使用SSE內嵌原語

  1. void CSSETestDlg::ComputeArrayCPlusPlusSSE(  
  2.           float* pArray1,                   // [in] first source array
  3.           float* pArray2,                   // [in] second source array
  4.           float* pResult,                   // [out] result array
  5.           int nSize)                        // [in] size of all arrays
  6. {  
  7.     int nLoop = nSize/ 4;  
  8.     __m128 m1, m2, m3, m4;  
  9.     __m128* pSrc1 = (__m128*) pArray1;  
  10.     __m128* pSrc2 = (__m128*) pArray2;  
  11.     __m128* pDest = (__m128*) pResult;  
  12.     __m128 m0_5 = _mm_set_ps1(0.5f);        // m0_5[0, 1, 2, 3] = 0.5
  13.     for ( int i = 0; i < nLoop; i++ )  
  14.     {  
  15.         m1 = _mm_mul_ps(*pSrc1, *pSrc1);        // m1 = *pSrc1 * *pSrc1
  16.         m2 = _mm_mul_ps(*pSrc2, *pSrc2);        // m2 = *pSrc2 * *pSrc2
  17.         m3 = _mm_add_ps(m1, m2);                // m3 = m1 + m2
  18.         m4 = _mm_sqrt_ps(m3);                   // m4 = sqrt(m3)
  19.         *pDest = _mm_add_ps(m4, m0_5);          // *pDest = m4 + 0.5
  20.         pSrc1++;  
  21.         pSrc2++;  
  22.         pDest++;  
  23.     }  
  24. }  


使用SSE彙編

  1. void CSSETestDlg::ComputeArrayAssemblySSE(  
  2.           float* pArray1,                   // [輸入] 源陣列1
  3.           float* pArray2,                   // [輸入] 源陣列2
  4.           float* pResult,                   // [輸出] 用來存放結果的陣列
  5.           int nSize)                        // [輸入] 陣列的大小
  6. {  
  7.     int nLoop = nSize/4;  
  8.     float f = 0.5f;  
  9.     _asm  
  10.     {  
  11.         movss   xmm2, f                         // xmm2[0] = 0.5
  12.         shufps  xmm2, xmm2, 0                   // xmm2[1, 2, 3] = xmm2[0]
  13.         mov         esi, pArray1                // 輸入的源陣列1的地址送往esi
  14.         mov         edx, pArray2                // 輸入的源陣列2的地址送往edx
  15.         mov         edi, pResult                // 輸出結果陣列的地址儲存在edi
  16.         mov         ecx, nLoop                  //迴圈次數送往ecx
  17. start_loop:  
  18.         movaps      xmm0, [esi]                 // xmm0 = [esi]
  19.         mulps       xmm0, xmm0                  // xmm0 = xmm0 * xmm0
  20.         movaps      xmm1, [edx]                 // xmm1 = [edx]
  21.         mulps       xmm1, xmm1                  // xmm1 = xmm1 * xmm1
  22.         addps       xmm0, xmm1                  // xmm0 = xmm0 + xmm1
  23.         sqrtps      xmm0, xmm0                  // xmm0 = sqrt(xmm0)
  24.         addps       xmm0, xmm2                  // xmm0 = xmm1 + xmm2
  25.         movaps      [edi], xmm0                 // [edi] = xmm0
  26.         add         esi, 16                     // esi += 16
  27.         add         edx, 16                     // edx += 16
  28.         add         edi, 16                     // edi += 16
  29.         dec         ecx                         // ecx--
  30.         jnz         start_loop                //如果不為0則轉向start_loop
  31.     }  
  32. }  


在訊號處理中的實際應用(sse2):

獲得訊號能量

  1. /* 
  2. * Compute Energy of a complex signal vector, removing the DC component!  
  3. * input  : points to vector 
  4. * length : length of vector in complex samples 
  5. */
  6. #define shift 4
  7. #define shift_DC 0
  8. int signal_energy(int *input, unsigned int length)  
  9. {  
  10.     int i;  
  11.     int temp, temp2;  
  12.     register __m64 mm0, mm1, mm2, mm3;  
  13.     __m64 *in;  
  14.     in = (__m64 *)input;  
  15.     mm0 = _m_pxor(mm0,mm0);  
  16.     mm3 = _m_pxor(mm3,mm3);  
  17.     for (i = 0; i < length >> 1; i++) {  
  18.         mm1 = in[i];  
  19.         mm2 = mm1;  
  20.         mm1 = _m_pmaddwd(mm1, mm1);  
  21.         mm1 = _m_psradi(mm1, shift);  
  22.         mm0 = _m_paddd(mm0, mm1);  
  23.         mm2 = _m_psrawi(mm2, shift_DC);  
  24.         mm3 = _m_paddw(mm3, mm2);  
  25.     }  
  26.     mm1 = mm0;  
  27.     mm0 = _m_psrlqi(mm0, 32);  
  28.     mm0 = _m_paddd(mm0, mm1);  
  29.     temp = _m_to_int(mm0);  
  30.     temp /= length;  
  31.     temp <<= shift;   
  32.     /*now remove the DC component*/
  33.     mm2 = _m_psrlqi(mm3, 32);  
  34.     mm2 = _m_paddw(mm2, mm3);  
  35.     mm2 = _m_pmaddwd(mm2, mm2);  
  36.     temp2 = _m_to_int(mm2);  
  37.     temp2 /= (length * length);  
  38.     temp2 <<= (2 * shift_DC);  
  39.     temp -= temp2;  
  40.     _mm_empty();  
  41.     _m_empty();  
  42.     return((temp > 0) ? temp : 1);  
  43. }  

基於SSE指令集的程式設計簡介


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 //