配色: 字号:
SSE指令集优化心得(一)
2018-10-08 | 阅:  转:  |  分享 
  
SSE指令集优化心得(一)背景SIMD(single-instruction,multiple-data)是一种使用单道指令处理多道数据流
的CPU执行模式,即在一个CPU指令执行周期内用一道指令完成处理多个数据的操作。常见的指令集MMX(Multi-MediaExt
ensions,多媒体扩展),主要问题是只对整数起作用,不支持浮点计算;SSE(StreamingSIMDExtensions
,单指令多数据流扩展),兼容MMX指令,可以提高浮点运算速度。SSE2、SSE3、SSE4(是SSE的扩展技术)3DNow!X8
6AVX(AdvancedVectorExtensions)沿用了的MMX/SSE指令集,指令格式上有一些变化,增强了SIMD
计算性能。问题最初,我们只能使用汇编语言来编写SIMD代码。不仅写起来很麻烦,而且易读性、可维护性、移植性都较差。不久,VC、GC
C等编译器相继支持了Intrinsic函数,使我们可以摆脱汇编,利用C语言来调用SIMD指令集,大大提高了易读性和可维护。而且移植
性也有提高,能在同一编译器上实现32位与64位的平滑过渡。但当代码在另一种编译器编译时,会遇到一些问题而无法编译。甚至在使用同一种
编译器的不同版本时,也会遇到无法编译问题。——首先是整数类型问题——传统C语言的short、int、long等整数类型是与平台
相关的,不同平台上的位长是不同的(例如Windows是LLP64模型,Linux、Mac等Unix系统多采用LP64模型)。而使用
SSE等SIMD指令集时需要精确计算数据的位数,不同位长的数据必须使用不同的指令来处理。有一个解决办法,就是使用C99标准中std
int.h所提供的指定位长的整数类型。GCC对C99标准支持性较好,而VC的步骤很慢,貌似直到VC2010才支持stdint.h。
而很多时候我们为了兼容旧代码,不得不使用VC6等老版本的VC编译器。——其次是Intrinsic函数的头文件问题——不同编译器所使
用的头文件不同——对于早期版本VC,需要根据具体的指令集需求,手动引入mmintrin.h、xmmintrin.h等头文件。对于V
C2005或更高版本,引入intrin.h就行了,它会自动引入当前编译器所支持的所有Intrinsic头文件。对于早期版本GCC,
也是手动引入mmintrin.h、xmmintrin.h等头文件。而对于高版本的GCC,引入x86intrin.h就行了,它会自动
引入当前编译环境所允许的Intrinsic头文件。——再次是当前编译环境下的Intrinsic函数集支持性问题——对于VC来说
,VC6支持MMX、3DNow!、SSE、SSE2,然后更高版本的VC支持更多的指令集。但是,VC没有提供检测Intrinsic函
数集支持性的办法。例如你在VC2010上编写了一段使用了AVXIntrinsic函数的代码,但拿到VC2005上就不能通过编译了
。其次,VC不支持64位下的MMX,这让一些老程序迁徙到64位版时遭来了一些麻烦。而对于GCC来说,它使用-mmmx、-mss
e等编译器开关来启用各种指令集,同时定义了对应的__MMX__、__SSE__等宏,然后x86intrin.h会根据这些宏来声明
相应的Intrinsic函数集。__MMX__、__SSE__等宏可以帮助我们判断Intrinsic函数集是否支持,但这只是GCC
的专用功能。此外还有一些细节问题,例如某些Intrinsic函数仅在64下才能使用、有些老版本编译器的头文件缺少某个Intri
nsic函数。所以我们希望有一种统一的方式来判断Intrinsic函数集的支持性。——除了编译期间的问题外,还有运行期间的问题—
—在运行时,怎么检测当前处理器支持哪些指令集?虽然X86体系提供了用来检测处理器的CPUID指令,但它没有规范的Intrin
sic函数,在不同的编译器上的用法不同。而且X86体系有很多种指令集,每种指令集具体的检测方法是略有区别的。尤其是SSE、AV
X这样的SIMD指令集是需要操作系统配合才能正常使用的,所以在CPUID检查通过后,还需要进一步验证。SSE介绍SSE(为Str
eamingSIMDExtensions的缩写)是由Intel公司,在1999年推出PentiumIII处理器时,
同时推出的新指令集,它是SIMD指令集扩展。SIMD(single-instruction,multiple-data)是一种使
用单道指令处理多道数据流的CPU执行模式,即在一个CPU指令执行周期内用一道指令完成处理多个数据的操作。?当对多个数据对象执行完全
相同的操作时,SIMD指令可以大大提高性能。典型的应用是数字信号处理和图形处理。SSE指令包括了四个主要的部份:单精度浮点数
运算指令、整数运算指令(此为MMX之延伸,并和MMX使用同样的缓存器)、Cache控制指令、和状态控制指令。这里主要是
介绍浮点数运算指令和Cache控制指令。intrinsic内联函数在C/C++程序中使用SSE指令有两种方式:直接嵌入汇编指令
(内嵌式汇编语言);使用编译器提供的支持SSE的intrinsics内联函数(从代码可读和维护角度讲,通过intrinsics内
联函数的形式来使用SSE更好)。/内嵌式汇编语言使用SSE指令集/_asmaddpsxmm0,xmm1__asm
movaps[ebx],xmm0...__m128data;...__asm{leaebx,dataaddpsxmm0,
xmm1movaps[ebx],xmm0}/通过intrinsics内联函数使用SSE指令集/__m128dat
a1,data2;...__m128out=_mm_add_ps(data1,data2);...intrinsics函
数是对MMX、SSE等指令集的一种封装,以函数的形式提供,在编译的时候,这些函数会被内联为汇编,不会产生函数调用的开销。头文件Vi
sualStudio使用SSE指令集需要添加对应的头文件:intrin.h-->AllArchitecturesmmint
rin.h-->MMXxmmintrin.h-->SSEemmintrin.h-->SSE2pmmintrin.h-
->SSE3smmintrin.h-->SSE4immintrin.h-->AVXSSE新增的寄存器(用于浮点运算指
令)SSE指令集支持的处理器有8个128位的寄存器(xmm0-xmm7),每一个寄存器可以存放4个(32位)单精度的浮点数。
SSE的浮点数运算指令就是使用这些寄存器。下图是SSE新增的寄存器的示意图:__m128数据类型SSE使用4个浮点数(43
2bit)组合成一个新的数据类型__m128,对应128位的寄存器。SSE指令的参数和返回结果的数据类型都是__m128。比如
:__m128_mm_add_ps(__m128a,__m128b);//两个四维向量相加SSE浮点运算指令分类pack
ed指令是一次对XMM寄存器中的四个浮点数(即DATA0~DATA3)均进行计算;scalar只对XMM暂存器中的DATA0进
行计算。SSE指令格式_mm__(参数表)前缀_mm,表示是SSE指令集对应的Intrinsi
c函数;表示指令的作用,比如加法add;是ps或者ss,分别表示为packed或者scalar;如
__m128_mm_add_ps(__m128a,__m128b);//两个四维向量相加内存对齐SSE指令要求处理的数据
16字节(128位二进制)对齐,也就是每16个字节分为一组。静态数组(staticarray)可由__declspec(alig
n(16))关键字声明:__declspec(align(16))floatm_fArray[ARRAY_SIZE];在xx
mintrin.h中定义了一个宏__MM_ALIGN16,所以上面的程序也可以写成:_MM_ALIGN16floatm_fAr
ray[ARRAY_SIZE];动态数组(dynamicarray)可由_aligned_malloc函数为其分配空间:m_fA
rray=(float)_aligned_malloc(ARRAY_SIZEsizeof(float),16);由
_aligned_malloc函数分配空间的动态数组可以由_aligned_free函数释放其占用的空间:_aligned_fre
e(m_fArray);以_mm_load_ps函数为例,其使用示例如下:这里加载正确的前提是:input这个浮点数阵列都是对齐在
16bytes的边上。如果没有对齐,就需要使用_mm_loadu_ps函数,这个函数用于处理没有对齐在16bytes上的数据,但
是其速度会比较慢。【注意】GCC编译器和VC编译器下字节对齐是不同的:GCC:__attribute__((aligned(
16)))VC:?__declspec(align(16))IntrinsicSSE相关指令Load系列(用于加载数据,从
内存到寄存器)__m128_mm_load_ss(floatp)__m128_mm_load_ps(float
p)__m128_mm_load1_ps(floatp)__m128_mm_loadh_pi(__m128a
,__m64p)__m128_mm_loadl_pi(__m128a,__m64p)__m128_mm
_loadr_ps(floatp)__m128_mm_loadu_ps(floatp)//不要求16
字节对齐Set系列(用于加载数据,从内存到寄存器,大部分需要多条指令完成,但是可能不需要16字节对齐)__m128_mm_set
_ss(floatw)__m128_mm_set_ps(floatz,floaty,floatx,float
w)__m128_mm_set1_ps(floatw)__m128_mm_setr_ps(floatz,flo
aty,floatx,floatw)__m128_mm_setzero_ps()Store系列(将计算结果从SS
E寄存器保存到内存)void_mm_store_ss(floatp,__m128a)void_mm_store_
ps(floatp,__m128a)void_mm_store1_ps(floatp,__m128a)
void_mm_storeh_pi(__m64p,__m128a)void_mm_storel_pi(__m
64p,__m128a)void_mm_storer_ps(floatp,__m128a)void_
mm_storeu_ps(floatp,__m128a)void_mm_stream_ps(floatp,
__m128a)算数指令SSE提供了大量的浮点运算指令,包括加法、减法、乘法、除法、开方、最大值、最小值、近似求倒数、求开方的倒
数等等。以加法为例:SSE中浮点加法的指令有:__m128_mm_add_ss(__m128a,__m128b)__
m128_mm_add_ps(__m128a,__m128b)参考:https://www.cnblogs.com/d
ragon2012/p/5200698.html实例使用SSE优化单精度浮点数组求和程序floatsumfloat_base(c
onstfloatpbuf,size_tcntbuf){//单精度浮点数组求和基本程序floatres=0;fo
r(size_ti=0;imfloat_4loop(constfloatpbuf,size_tcntbuf){//单精度浮点数组求和4
路循环展开程序floatres=0;floatfsum0=0,fsum1=0,fsum2=0,fsum3=
0;size_ti=0;constfloatp=pbuf;for(i=0;ifsum0+=p[i];fsum1+=p[i+1];fsum2+=p[i+2];fsum3+=p[i+3];}re
s=fsum0+fsum1+fsum2+fsum3;//merge/remainder/for(;
ifloatpbuf,size_tcntbuf){//单精度浮点数组求和SSE优化程序floatres=0;s
ize_ti;size_tnBlockWidth=4;size_tcntBlock=cntbuf/nBlockW
idth;size_tcntRem=cntbuf%nBlockWidth;//remainder__m128xfs
Sum=_mm_setzero_ps();//init__m128xfsLoad;constfloatp=pb
uf;//PointerusedinSSEbatchprocessingconstfloatq;//Point
erusedinmergingSSEvariable./SSEbatchprocessing/for
(i=0;ium=_mm_add_ps(xfsSum,xfsLoad);//addp+=nBlockWidth;}/m
ergingSSEvariable/q=(constfloat)&xfsSum;res=q[0]+q[1
]+q[2]+q[3];/remainder/for(i=0;i[i];returnres;}floatsumfloat_sse_4loop(constfloatpbuf,siz
e_tcntbuf){//单精度浮点数组求和SSE优化+4路循环展开程序floatres;size_ti;size
_tnBlockWidth=44;//SSEregisterprocess4floatsatime,
andLoopexpansion4timessize_tcntBlock=cntbuf/nBlockWidth
;size_tcntRem=cntbuf%nBlockWidth;//remainder__m128xfsSum0
=_mm_setzero_ps();//init__m128xfsSum1=_mm_setzero_ps();__m1
28xfsSum2=_mm_setzero_ps();__m128xfsSum3=_mm_setzero_ps();_
_m128xfsLoad0;//load__m128xfsLoad1;__m128xfsLoad2;__m128xf
sLoad3;constfloatp=pbuf;//PointerusedinSSEbatchproces
singconstfloatq;//PointerusedinmergingSSEvariable./S
SEbatchprocessing/for(i=0;i_mm_load_ps(p);//loadxfsLoad1=_mm_load_ps(p+4);xfsLoad2=_
mm_load_ps(p+8);xfsLoad3=_mm_load_ps(p+12);xfsSum0=_mm_a
dd_ps(xfsSum0,xfsLoad0);//addxfsSum1=_mm_add_ps(xfsSum1,
xfsLoad1);xfsSum2=_mm_add_ps(xfsSum2,xfsLoad2);xfsSum3=_mm_
add_ps(xfsSum3,xfsLoad3);p+=nBlockWidth;}/mergingSSEvaria
ble/xfsSum0=_mm_add_ps(xfsSum0,xfsSum1);xfsSum2=_mm_ad
d_ps(xfsSum2,xfsSum3);xfsSum0=_mm_add_ps(xfsSum0,xfsSum2);q=(
constfloat)&xfsSum0;res=q[0]+q[1]+q[2]+q[3];/remainde
r/for(i=0;i运行结果:Addavector:----------ElapsedTiming(Cycles):250200------
----------------------------------Addavectorwith4loops:-----
-----ElapsedTiming(Cycles):84942------------------------------
----------AddavectorusingSSE:----------ElapsedTiming(Cycles)
:62734----------------------------------------Addavectorusin
gSSEwith4loops:----------ElapsedTiming(Cycles):24967------
----------------------------------可以看到,加入SSE指令后的程序跟基本程序相比,性能差不
多提高了4倍,加入SSE指令集同时做4路循环展开的性能差不多提高了10倍。SSE总结SSE最强大的是其能够在一条指令并行的对多个操
作数进行相同的运算。在使用SSE指令时要特别注意操作数的类型,整型则要区分是有符号还是无符号;浮点数则注意其精度是单精度还是双精度。C/C++使用SSE指令集有两种方式,直接嵌入汇编指令,或者使用intrinsic内联函数。前者优化效果最好,后者代码更易于可读和维护。注意用于并行计算的数据结构要16字节对齐。参考http://dev.gameres.com/Program/Other/SSEjianjie.htmhttp://dev.gameres.com/Program/Other/SSEjianjie.htmhttps://blog.csdn.net/gengshenghong/article/details/7008704https://blog.csdn.net/gengshenghong/article/details/7008704https://www.cnblogs.com/dragon2012/p/5200698.htmlhttps://www.cnblogs.com/dragon2012/p/5200698.htmlhttps://blog.csdn.net/tina_j/article/details/37901035https://blog.csdn.net/tina_j/article/details/379010350
献花(0)
+1
(本文系赶紧_学习原创)