SSE加速指令集

来源:互联网 发布:java jar 打包 编辑:程序博客网 时间:2024/06/10 00:43

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_fMaxvoid 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_fMaxvoid 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_fMaxvoid 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])));}
   #define WIN  #include <Windows.h>#include <intrin.h>  #include <stdlib.h>  #include <math.h>  #include <iostream>using namespace std;#define N 4*1000000    // 注意:必须是4的倍数,否则使用SSE指令计算,要进行一些处理,从而保证正确。  _MM_ALIGN16 float op1[N];//---dymic array memory align//float* op1 = (float*)_aligned_malloc(N * sizeof(float), 16);//_aligned_free(op1);_MM_ALIGN16 float op2[N];_MM_ALIGN16 float result1[N];_MM_ALIGN16 float result2[N];/*//---指定字节对齐__declspec(align(16)) float op1[4] = {1.0, 2.0, 3.0, 4.0};__declspec(align(16)) float op2[4] = {1.0, 2.0, 3.0, 4.0};_MM_ALIGN16 float result[4];        // A macro, same as "__declspec(align(16))"*/void init(){    for (int i = 0; i < N; i++)    {        op1[i] = (float)rand() / (float)RAND_MAX;        op2[i] = (float)rand() / (float)RAND_MAX;    }}void checkResult(int debug){    bool isSame = true;    for (int i = 0; i < N; i++)    {        if (debug)        {            cout << "result1:" << result1[i] << "result2:" << result2[i] << endl;        }        else        {            if (fabs(result1[i] - result2[i]) > 0.000001)            {                isSame = false;                break;            }        }    }    if (!debug) {        if (isSame)            printf("Result is Same\n");        else            printf("Result is not same\n");    }}void add1(){    for (int i = 0; i < N; i++)    {        //op1[i] = (float)rand() / (float)RAND_MAX;        //op2[i] = (float)rand() / (float)RAND_MAX;        result1[i] = op1[i] + op2[i];    }}void add2(){    __m128  a;    __m128  b;    __m128  c;    //for (int i = 0; i < N; i = i + 4)    for (int i = 0; i < N/4; i++)    {        /*        op1[i] = (float)rand() / (float)RAND_MAX;        op2[i] = (float)rand() / (float)RAND_MAX;        __m128  a;        __m128  b;        __m128  c;        // Load          a = _mm_loadu_ps(op1);        b = _mm_loadu_ps(op2);        // Calculate          c = _mm_add_ps(a, b);        */        //---由SSE指令处理的每一个浮点数数组必须把其中需要处理的数每16个字节(128位二进制)分为一组        // Load          /*a = _mm_load_ps(op1 +i);//0.00869        b = _mm_load_ps(op2 +i);*/        a = _mm_load_ps(op1);//0.00326        b = _mm_load_ps(op2);        //一次会将同时将数组中的4个元素进行运算        c = _mm_add_ps(a, b);   // c = a + b  //---//将4对32位浮点数同时进行相加操作。这4对32位浮点数来自两个128位的存储单元,再把计算结果(相加之和)赋给一个128位的存储单元。        //_mm_store_ps(result2 +i, c);        _mm_store_ps(result2, c);    }}void add3(){    //---申请内存空间    float op1[4] = { 1.0, 2.0, 3.0,4.0};    float op2[4] = { 1.0, 2.0, 3.0, 4.0};    float result[4];    //---申请寄存器内存空间    __m128  a;    __m128  b;    __m128  c;    // Load---从内存到寄存器中      a = _mm_loadu_ps(op1);    b = _mm_loadu_ps(op2);    // Calculate      c = _mm_add_ps(a, b);   // c = a + b      // Store      _mm_storeu_ps(result, c);    /*      // Using the __m128 union to get the result.    printf("0: %lf\n", c.m128_f32[0]);    printf("1: %lf\n", c.m128_f32[1]);    printf("2: %lf\n", c.m128_f32[2]);    printf("3: %lf\n", c.m128_f32[3]);    */    printf("0: %lf\n", result[0]);    printf("1: %lf\n", result[1]);    printf("2: %lf\n", result[2]);    printf("3: %lf\n", result[3]);}void add4(    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++;    }}void add5(    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++;    }}void OnBnClickedButtonCplusplus(){    float m_fMin = FLT_MAX;    float m_fMax = FLT_MIN;    const int ARRAY_SIZE = 4 * 100000;    int i;    float* m_fResultArray;    float* m_fInitialArray;    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];    }}void OnBnClickedButtonSseC(){    const int ARRAY_SIZE = 4 * 100000;    float* m_fResultArray;    float* m_fInitialArray;    __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;    //__m128* pSource;    //pSource = _mm_load_ps(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可以节省内存    1. 共用体和结构体都是由多个不同的数据类型成员组成, 但在任何同一时刻,     共用体只存放了一个被选中的成员, 而结构体的所有成员都存在。    2. 对于共用体的不同成员赋值, 将会对其它成员重写, 原来成员的值就不存在了,    而对于结构体的不同成员赋值是互不影响的。    */    union u    {        __m128 m;        float f[4];    } x;    x.m = min128;    //---获取数组中的最小值    int m_fMin = min(x.f[0], min(x.f[1], min(x.f[2], x.f[3])));    x.m = max128;    int m_fMax = max(x.f[0], max(x.f[1], max(x.f[2], x.f[3])));}int main(){    init();    _LARGE_INTEGER time_start;    /*开始时间*/    _LARGE_INTEGER time_over;        /*结束时间*/    double dqFreq;                /*计时器频率*/    LARGE_INTEGER f;            /*计时器频率*/    QueryPerformanceFrequency(&f);    dqFreq = (double)f.QuadPart;    QueryPerformanceCounter(&time_start);    //---    printf("Add a vector:\n");    add1();    //    QueryPerformanceCounter(&time_over);    cout << ((time_over.QuadPart - time_start.QuadPart) / dqFreq) << endl;//单位为秒,精度为1000 000/(cpu主频)微秒    printf("\n");    _LARGE_INTEGER time_start1;    /*开始时间*/    _LARGE_INTEGER time_over1;        /*结束时间*/    double dqFreq1;                /*计时器频率*/    LARGE_INTEGER f1;            /*计时器频率*/    QueryPerformanceFrequency(&f1);    dqFreq1 = (double)f.QuadPart;    QueryPerformanceCounter(&time_start1);    printf("Add a vector with SSE instructions:\n");    add2();    QueryPerformanceCounter(&time_over1);    cout << ((time_over1.QuadPart - time_start1.QuadPart) / dqFreq1) << endl;    printf("\n");    checkResult(0);    add3();    return 0;}/*//---#include <iostream>#include <windows.h>using namespace    std;void main(){_LARGE_INTEGER time_start;     //开始时间_LARGE_INTEGER time_over;      //结束时间double dqFreq;                //计时器频率LARGE_INTEGER f;              //计时器频率QueryPerformanceFrequency(&f);dqFreq = (double)f.QuadPart;QueryPerformanceCounter(&time_start);Sleep(1000);//循环耗时QueryPerformanceCounter(&time_over);cout << ((time_over.QuadPart - time_start.QuadPart) / dqFreq) << endl;//单位为秒,精度为1000 000/(cpu主频)微秒}//---*/
转自:http://dev.gameres.com/Program/Other/sseintro.htm
0 0
原创粉丝点击