基于SSE指令集的程序设计简介

来源:互联网 发布:山西省经信委大数据 编辑:程序博客网 时间:2024/05/27 02:27

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])));

}

原创粉丝点击