1. 程式人生 > >SSE指令集 c,c++程式程式碼優化

SSE指令集 c,c++程式程式碼優化

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

[5] Matt Pietrek在Microsoft Systems Journal 1998年2月刊上的評論文章:
http://www.microsoft.com/msj/0298/hood0298.aspx 。