SSE 使用总结

背景

SIMD(single-instruction, multiple-data)是一种使用单道指令处理多道数据流的 CPU 执行模式,即在一个 CPU 指令执行周期内用一道指令完成处理多个数据的操作。

常见的指令集

• MMX(Multi-Media Extensions,多媒体扩展),主要问题是只对整数起作用,不支持浮点计算;

• SSE(Streaming SIMD Extensions,单指令多数据流扩展),兼容 MMX 指令,可以提高浮点运算速度。

• SSE2、SSE3、SSE4 (是 SSE 的扩展技术)

• 3DNow!

• X86

• AVX(Advanced Vector Extensions)沿用了的 MMX/SSE 指令集,指令格式上有一些变化,增强了 SIMD 计算性能。  

 

问题

最初,我们只能使用汇编语言来编写 SIMD 代码。不仅写起来很麻烦,而且易读性、可维护性、移植性都较差。不久,VC、GCC 等编译器相继支持了 Intrinsic 函数,使我们可以摆脱汇编,利用 C 语言来调用 SIMD 指令集,大大提高了易读性和可维护。而且移植性也有提高,能在同一编译器上实现32 位与 64 位的平滑过渡。但当代码在另一种编译器编译时,会遇到一些问题而无法编译。甚至在使用同一种编译器的不同版本时,也会遇到无法编译问题。

 ——首先是整数类型问题——  

传统 C 语言的 short、int、long 等整数类型是与平台相关的,不同平台上的位长是不同的(例如 Windows 是 LLP64 模型,Linux、Mac 等 Unix 系统多采用 LP64 模型)。而使用 SSE 等 SIMD 指令集时需要精确计算数据的位数,不同位长的数据必须使用不同的指令来处理。有一个解决办法,就是使用 C99 标准中 stdint.h 所提供的指定位长的整数类型。GCC 对 C99 标准支持性较好,而 VC 的步骤很慢,貌似直到 VC2010 才支持 stdint.h。而很多时候我们为了兼容旧代码,不得不使用 VC6等老版本的 VC 编译器。

  

——其次是 Intrinsic 函数的头文件问题——

不同编译器所使用的头文件不同——对于早期版本 VC,需要根据具体的指令集需求,手动引入mmintrin.h、xmmintrin.h 等头文件。对于 VC2005 或更高版本,引入 intrin.h 就行了,它会自动引入当前编译器所支持的所有 Intrinsic 头文件。对于早期版本 GCC,也是手动引入mmintrin.h、xmmintrin.h 等头文件。而对于高版本的 GCC,引入 x86intrin.h 就行了,它会自动引入当前编译环境所允许的 Intrinsic 头文件。   

 

——再次是当前编译环境下的 Intrinsic 函数集支持性问题——  

对于 VC 来说,VC6 支持 MMX、3DNow!、SSE、SSE2,然后更高版本的 VC 支持更多的指令集。但是,VC 没有提供检测 Intrinsic 函数集支持性的办法。例如你在 VC2010 上编写了一段使用了 AVX Intrinsic 函数的代码,但拿到 VC2005 上就不能通过编译了。其次,VC 不支持 64 位下的 MMX,这让一些老程序迁徙到 64 位版时遭来了一些麻烦。  而对于 GCC 来说,它使用-mmmx、-msse 等编译器开关来启用各种指令集,同时定义了对应的 __MMX__、__SSE__等宏,然后 x86intrin.h 会根据这些宏来声明相应的 Intrinsic 函数集。__MMX__、__SSE__等宏可以帮助我们判断 Intrinsic 函数集是否支持,但这只是 GCC 的专用功能。  此外还有一些细节问题,例如某些 Intrinsic 函数仅在 64 下才能使用、有些老版本编译器的头文件缺少某个 Intrinsic 函数。所以我们希望有一种统一的方式来判断 Intrinsic 函数集的支持性。   

 

——除了编译期间的问题外,还有运行期间的问题——  

在运行时,怎么检测当前处理器支持哪些指令集?  

虽然 X86 体系提供了用来检测处理器的 CPUID 指令,但它没有规范的 Intrinsic 函数,在不同的编译器上的用法不同。  

而且 X86 体系有很多种指令集,每种指令集具体的检测方法是略有区别的。尤其是 SSE、AVX这样的 SIMD 指令集是需要操作系统配合才能正常使用的,所以在 CPUID 检查通过后,还需要进一步验证。

SSE 介绍

SSE(为 Streaming SIMD Extensions 的缩写)是由 Intel 公司,在 1999 年推出 Pentium III 处理器时,同时推出的新指令集,它是 SIMD 指令集扩展。SIMD(single-instruction, multiple-data)是一种使用单道指令处理多道数据流的 CPU 执行模式,即在一个 CPU 指令执行周期内用一道指令完成处理多个数据的操作。 当对多个数据对象执行完全相同的操作时, SIMD 指令可以大大提高性能。典型的应用是数字信号处理和图形处理。  

 SSE 指令包括了四个主要的部份:单精度浮点数运算指令、整数运算指令(此为 MMX 之延伸,并和 MMX 使用同样的缓存器)、Cache 控制指令、和状态控制指令。 这里主要是介绍浮点数运算指令和 Cache 控制指令。

 

 intrinsic 内联函数

在 C/C++程序中使用 SSE 指令有两种方式:

• 直接嵌入汇编指令(内嵌式汇编语言);

• 使用编译器提供的支持 SSE 的 intrinsics 内联函数 (从代码可读和维护角度讲,通过intrinsics 内联函数的形式来使用 SSE 更好)。

 /** 内嵌式汇编语言使用 SSE 指令集 **/

_asm addps xmm0, xmm1

__asm movaps[ebx], xmm0

...

 __m128 data;

 ...

 __asm

{

 lea ebx, data addps

xmm0, xmm1 m

ovaps[ebx], xmm0

}

/** 通过 intrinsics 内联函数使用 SSE 指令集 **/

__m128 data1, data2;

...

__m128 out = _mm_add_ps(data1, data2);

 ...  

intrinsics 函数是对 MMX、SSE 等指令集的一种封装,以函数的形式提供,在编译的时候,这些函数会被内联为汇编,不会产生函数调用的开销。 头文件 Visual Studio 使用 SSE 指令集需要添加对应的头文件:

intrin.h --> All Architectures

mmintrin.h --> MMX

xmmintrin.h --> SSE

emmintrin.h --> SSE2

pmmintrin.h --> SSE3

smmintrin.h --> SSE4

 immintrin.h --> AVX  

SSE 新增的寄存器(用于浮点运算指令)

SSE 指令集支持的处理器有 8 个 128 位的寄存器( xmm0 -xmm7 ),每一个寄存器可以存放 4 个(32 位)单精度的浮点数。

SSE 的浮点数运算指令就是使用这些寄存器。

下图是 SSE 新增的寄存器的示意图:

    __m128 数据类型 SSE 使用 4 个浮点数(4*32bit)组合成一个新的数据类型__m128 ,对应 128 位的寄存器。SSE 指令的参数和返回结果的数据类型都是__m128 。

比如:

__m128 _mm_add_ps(__m128 a, __m128 b); //两个四维向量相加  

 

SSE 浮点运算指令分类

• packed 指令是一次对 XMM 寄存器中的四个浮点数(即 DATA0 ~ DATA3)均进行计算; •scalar 只对 XMM 暂存器中的 DATA0 进行计算。  

 

SSE 指令格式 _mm_<opcode>_<suffix> (参数表)

• 前缀_mm,表示是 SSE 指令集对应的 Intrinsic 函数;  

• <opcode>表示指令的作用,比如加法 add;

• <suffix>是 ps 或者 ss,分别表示为 packed 或者 scalar;

如 __m128 _mm_add_ps(__m128 a, __m128 b);//两个四维向量相加  

内存对齐

• SSE 指令要求处理的数据 16 字节(128 位二进制)对齐,也就是每 16 个字节分为一组。

• 静态数组(static array)可由__declspec(align(16))关键字声明:

__declspec(align(16)) float m_fArray[ARRAY_SIZE];

• 在 xxmintrin.h 中定义了一个宏__MM_ALIGN16,所以上面的程序也可以写成: _MM_ALIGN16 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);  

• 以_mm_load_ps函数为例,其使用示例如下:    

这里加载正确的前提是:input这个浮点数阵列都是对齐在16 bytes的边上。如果没有对齐,就需要使用_mm_loadu_ps函数,这个函数用于处理没有对齐在16bytes上的数据,但是其速度会比较慢。  

【注意】GCC编译器和VC编译器下字节对齐是不同的:  

GCC : __attribute__((aligned(16)))  

VC : __declspec(align(16))  

Intrinsic SSE 相关指令 Load系列(用于加载数据,从内存到寄存器) ·

__m128 _mm_load_ss (float *p) ·

__m128 _mm_load_ps (float *p) ·

__m128 _mm_load1_ps (float *p) ·

__m128 _mm_loadh_pi (__m128 a, __m64 *p) ·

__m128 _mm_loadl_pi (__m128 a, __m64 *p) ·

__m128 _mm_loadr_ps (float *p) ·

__m128 _mm_loadu_ps (float *p) // 不要求16字节对齐  

 

Set系列(用于加载数据,从内存到寄存器,大部分需要多条指令完成,但是可能不需要16字节对齐) ·

 __m128 _mm_set_ss (float w) ·

__m128 _mm_set_ps (float z, float y, float x, float w) ·

__m128 _mm_set1_ps (float w) ·

__m128 _mm_setr_ps (float z, float y, float x, float w) ·

__m128 _mm_setzero_ps ()  

 

Store系列(将计算结果从SSE寄存器保存到内存) ·

void _mm_store_ss (float *p, __m128 a) ·

void _mm_store_ps (float *p, __m128 a) ·

void _mm_store1_ps (float *p, __m128 a) ·

void _mm_storeh_pi (__m64 *p, __m128 a) ·

void _mm_storel_pi (__m64 *p, __m128 a) ·

void _mm_storer_ps (float *p, __m128 a) ·

void _mm_storeu_ps (float *p, __m128 a) ·

void _mm_stream_ps (float *p, __m128 a)  

 

算数指令 ·

SSE提供了大量的浮点运算指令,包括加法、减法、乘法、除法、开方、最大值、最小值、近似求倒数、求开方的倒数等等。以加法为例: ·

 SSE中浮点加法的指令有: ·

__m128 _mm_add_ss (__m128 a, __m128 b) ·

__m128 _mm_add_ps (__m128 a, __m128 b)  

 

参考:https://www.cnblogs.com/dragon2012/p/5200698.html  

实例

使用 SSE 优化单精度浮点数组求和程序

 

float sumfloat_base(const float* pbuf,size_t cntbuf)

{

//单精度浮点数组求和 基本程序

float res = 0;

for(size_t i=0 ; i<cntbuf ; i++)

res += pbuf[i]; return res;

}   

 

float sumfloat_4loop(const float* pbuf , size_t cntbuf)

{

//单精度浮点数组求和 4 路循环展开程序

float res = 0;

float fsum0 = 0, fsum1 =0, fsum2 =0, fsum3 =0;

size_t i=0;

const float* p = pbuf;

for( i=0 ; i<cntbuf-4 ; i+=4)

{

 fsum0 += p[i];

fsum1 += p[i+1];

 fsum2 += p[i+2];

fsum3 += p[i+3];

}  

 res = fsum0 + fsum1 + fsum2 + fsum3 ; //merge  

/* remainder */

for( ; i<cntbuf ; i++)  

res += p[i];

return res;

}   

 

float sumfloat_sse(const float* pbuf , size_t cntbuf)

{

//单精度浮点数组求和 SSE优化程序  

float res = 0;  

size_t i;

size_t nBlockWidth = 4;  

size_t cntBlock = cntbuf / nBlockWidth;  

size_t cntRem = cntbuf % nBlockWidth; // remainder  

__m128 xfsSum = _mm_setzero_ps();

// init  

__m128 xfsLoad;  

const float* p = pbuf; //Pointer used in SSE batch processing  

const float* q; //Pointer used in merging SSE variable.   

/* SSE batch processing */  

for(i=0 ; i<cntBlock ; i++)

{  

xfsLoad = _mm_load_ps(p); // load  

xfsSum = _mm_add_ps(xfsSum , xfsLoad); // add  

p += nBlockWidth;  

}   

/* merging SSE variable */  

q = (const float*)&xfsSum;  

res = q[0] + q[1] + q[2] + q[3];   

/* remainder */  

for(i=0; i<cntRem ; i++)  

res += p[i];  

return res;

}  

 float sumfloat_sse_4loop(const float* pbuf , size_t cntbuf)

{

//单精度浮点数组求和 SSE 优化+4 路循环展开程序

float res;

size_t i;

size_t nBlockWidth = 4 * 4; //SSE register process 4 floats a time, and Loop expansion 4 times size_t cntBlock = cntbuf / nBlockWidth;

size_t cntRem = cntbuf % nBlockWidth; // remainder

 __m128 xfsSum0 = _mm_setzero_ps(); // init

__m128 xfsSum1 = _mm_setzero_ps();

__m128 xfsSum2 = _mm_setzero_ps();

__m128 xfsSum3 = _mm_setzero_ps();

__m128 xfsLoad0; //load

 __m128 xfsLoad1;

__m128 xfsLoad2;

__m128 xfsLoad3;

const float* p = pbuf; //Pointer used in SSE batch processing  

 const float* q; //Pointer used in merging SSE variable.  

/* SSE batch processing */

for(i=0 ; i<cntBlock ; i++)

{

xfsLoad0 = _mm_load_ps(p); // load

xfsLoad1 = _mm_load_ps(p + 4);

xfsLoad2 = _mm_load_ps(p + 8);

xfsLoad3 = _mm_load_ps(p + 12);  

xfsSum0 = _mm_add_ps(xfsSum0 , xfsLoad0); // add

xfsSum1 = _mm_add_ps(xfsSum1 , xfsLoad1);

xfsSum2 = _mm_add_ps(xfsSum2 , xfsLoad2);

xfsSum3 = _mm_add_ps(xfsSum3 , xfsLoad3);  

p += nBlockWidth;

}  

/* merging SSE variable */

xfsSum0 = _mm_add_ps(xfsSum0,xfsSum1);

xfsSum2 = _mm_add_ps(xfsSum2,xfsSum3);

xfsSum0 = _mm_add_ps(xfsSum0,xfsSum2);

q = (const float*)&xfsSum0;

res = q[0] + q[1] + q[2] + q[3];  /* remainder */

for(i=0; i<cntRem ; i++)  

res += p[i];

return res;

}  

性能测试 在“元”上的运行结果:

Add a vector:

----------Elapsed Timing(Cycles) : 250200 ----------------------------------------

Add a vector with 4 loops:

----------Elapsed Timing(Cycles) : 84942 ----------------------------------------

Add a vector using SSE:

----------Elapsed Timing(Cycles) : 62734 ----------------------------------------

Add a vector using SSE with 4 loops:

----------Elapsed Timing(Cycles) : 24967 ----------------------------------------  

可以看到,加入 SSE 指令后的程序 跟 基本程序相比 ,性能差不多提高了 4 倍,加入 SSE 指令集同时做 4 路循环展开的性能差不多提高了 10 倍。

SSE 总结 • SSE 最强大的是其能够在一条指令并行的对多个操作数进行相同的运算。  

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值