SIMD单指令流多数据流(SingleInstruction Multiple Data,SIMD)是一种采用一个控制器来控制多个处理器,同时对一组数据(又称“数据向量”)中的每一个分别执行相同的操作从而实现空间上的并行性的技术。在微处理器中,单指令流多数据流技术则是一个控制器控制多个平行的处理微元,例如Intel的MMX或SSE以及AMD的3D Now!技术。
--------------------------------------------------------------------------------------------------------------------------------------------------------------------
MMX是由英特尔开发的一种SIMD多媒体指令集,共有57条指令。它于1996年集成在英特尔奔腾 (Pentium) MMX处理器上,以提高其多媒体数据的处理能力。
其优点是增加了处理器关于多媒体方面的处理能力,缺点是占用浮点数寄存器进行运算(64位MMX寄存器实际上就是浮点数寄存器的别名)以至于MMX指令和浮点数操作不能同时工作。为了减少在MMX和浮点数模式切换之间所消耗的时间,程序员们尽可能减少模式切换的次数,也就是说,这两种操作在应用上是互斥的。后来英特尔在此基础上发展出SSE指令集;AMD在此基础上发展出3DNow!指令集。现在新开发的程序不再仅使用MMX来优化软件执行效能,而是改使用如SSE、3DNOW!等更容易优化效能的新一代多媒体指令集,不过目前的处理器仍可以执行针对MMX优化的较早期软件。
SIMD是指单指令多数据技术,它已经成为Intel处理器的重要性能扩展。目前Intel处理器支持的SIMD技术包括MMX,SSE,AVX.
MMX提供了8个64bit的寄存器进行SIMD操作,SSE系列提供了128bit的8个寄存器进行SIMD指令操作。而最新的AVX指令则支持256bit的SIMD操作。
目前SIMD指令可以有四种方法进行使用分别是汇编语言,C++类,编译器Intrisincs和自动矢量化。我们用下面的求一个整数数组的和作为例子:
- int SumAarray(int *buf,int N)
- {
- int i,sum=0;
- for(i=0;i<N;i++)
- sum+=buf[i];
-
- return sum;
- }
方法一:自动矢量化
Intel编译器支持自动矢量化,通过选择特定的处理器选项/Q[a]{T|P|N|W|K},上面的代码可以采用下来命令进行编译:
icl /QxP/c /Fa test_vec.c
test_vec.c(4):(col.1)remark:LOOP WAS VECTORIZED.从上面的报告中可以看到第四行的循环自动向量化,还可以通过生成的汇编代码,看到使用了SIMD指令。
方法二:C++类
Intel C++编译器和C++类库中提供了一些新的C++类,这些类对应那些可以直接使用的SIMD指令(支持MMX,SSE,SSE2,不支持SSE3)的数据类型,使用时需要包含如下头文件:
- #include <ivec.h>//MMX
- #include <fvec.h>//SSE(also include ivec.h)
- #include <dvec.h>//SSE2(also include fvec.h)
这些支持SIMD的向量类型采取下面的命名规则:前面用I和F分别表示是支持浮点还是整数SIMD指令,接下来是数字取值为8,16,32,64,表示组向量的基本元素大小。然后后面为字符串vec,最后的数组取值为8,4,2,1,表示组成向量的基本元素的个数。使用64bit的MMX技术的整数类包括I64vec1,I32vec2,I16vec4和I8vec8,而使用128bit的XMM寄存器的浮点类则包括F32vec4,F32vec1,F64vec2。SSE2中使用128bit的XMM寄存器,整数类包括:I128vec1,I64vec2,I32vec4,I16vec8,I8vec16,为了进一步区分封装的是有符号整数还是无符号整数,在那些整数之后也可以包含一个符号标志s或者u,比如Iu32vec4.
通过类的封装,程序员无须关心那些对于类的运算到底使用了哪些汇编指令或者SIMD intrinsic函数,应用易于阅读和编码,并且没有直接使用SIMD代码,在不同的处理器之间不需要任何改动,但是其缺点是无法访问所有的指令和数据类型的组合。
下面的代码给出了SumAarray采用C++类的实现。
- #include
- int SumAarray(int *buf,int N)
- {
- int i;
- I32vec4 *vec4 = (I32vec4 *)buf;
- I32vec4 sum(0,0,0,0);
- for(i=0;i<N;i++)
- sum += vec4[i];
- return sum[0]+sum[1]+sum[2]+sum[3];
- }
方法三:使用intrinsics
SIMD intrinsics有些类似于C语言中的函数,可以被其它的代码直接调用。可以像其它函数调用一样给它传递参数,Intel C++编译器支持SIMD intrinsics,并且可以针对intrinsics函数进行内联等优化。intrinsics能够被直接映射到一条或者多条SIMD指令以及其它汇编指令。至于寄存器分配、指令调度和寻址模式,则留给编译器处理。因此,相比汇编语言来说更容易使用。但是,对于生成指令的控制能力则更小。为了使用与SIMD技术相关的intrinsics,首先需要包含那些定义了数据类型和函数的头文件。
- #include <mmintrin.h>//mmx
- #include <xmmintrin.h>//sse
- #include <emmintrin.h>//sse2
- #include <pmmintrin.h>//sse3
这些头文件定义了一些数据类型对应于那些SIMD指令要使用的封装浮点数和整数,这些数据类型名以两个下划线开始:
__m64用于表示一个MMX寄存器,表示封装了8个8bit,4个16bit,2个32bit,1个64bit的整数;_
_m128用于SSE,表示封装了;4个32bit的单精度浮点数;
_m128d可以封装2个64bit的双精度浮点数;
_m128i用于表示128bit SIMD整数运算的XMM寄存器。
__m128,__m128d,__m128i的内存变量位于16byte的边界;
- int SumArray(int *buf,int N)
- {
- int i;
- __m128i *vec128 = (__m128i *)buf;
- __m128 sum;
- sum = _mm_sub_epi32(sum,sum);
- for(i=0;i<N/4;i++)
- sum = _mm_add_epi32(sum,vec128[i]);
- sum = _mm_add_epi32(sum,_mm_srli_si128(sum,8));
- sum = _mm_add_epi32(sum,_mm_srli_si128(sum,4));
- return _mm_cvtsi128_si32(sum);
- }
- }
我们再来看一段代码,注意在循环体有一个条件语句,使用/QxP选项进行编译会发现Intel编译器并不会进行自动矢量化。
return sum[0]+sum[1]+sum[2]+sum[3];
}
假设上述代码中的N=1000,那么原来的代码需要进行1000次加法,而现在循环中只需要250次加法,在return的时候多了额外四个加法。此外,循环次数从1000次减少到250次,减少了很多跳转指令。
方法三:Intrinsics
SIMD intrinsics有一些类似于C语言中的函数,可以被其它代码直接调用,可以像其它函数一样给它传递参数,Intel C++编译器支持SIMD intrinsics(VS2005/2010也支持,GCC也支持),并且可以针对函数进行内联等优化。需要包含的头文件:
#include //MMX
#include //SSE (include mmintrin.h)
#include //SSE2 (include xmmintrin.h)
#include //SSE3 (include emmintrin.h)
这些头文件定了一些数据类型对应于那些SIMD指令要适应的浮点数和整数。这些数据类型名以两个下划线开始:
__m64用于表示一个MMX寄存器的内容,用于64bi的MMX整数指令,可以封装正8个8bit,4个16bit,2个32bit和一个64bit的整数。
__m128用于SSE,表示封装了四个32bit的单精度浮点数的SSE寄存器。
__m128d可以封装2个64bit的双精度浮点数;__m128i用于表示支持128bi的内存变量,位于16B的边界。声明为__m64的内存变量位于8B的边界。
注意:定义的intrinsics数据类型,并不是一个标准的C数据类型,所以,这些变量只能用于赋值语句的左边,返回值或者函数调用参数,不能进行加法减法等操作。
SIMD intrinsics的SumArray函数的实现:
int SumArray(int *buf,int N)
{
int i;
__m128i *vec128 = (__128i *)buf;
__m128i sum;
sum = _mm_sub_epi32(sum,sum);//set to zero 这个方法其它SMD置零操作谁更快呢?
for(i=0;i
sum = _mm_add_epi32(sum,_mm_srli_si128(sum,8));
sum = _mm_add_epi32(sum,_mm_srli_si128(sum,4));
return _mmcvtsi128_si32(sum);
}
SMD intrinsics函数采用一个非常标准的命名格式,大部分采取:_mm__的格式,函数名以_mm开头,然后表示函数要执行的SIMD指令,比如,上面的add,sub,srli分别表示加法,减法,以为运算,最后是后缀,后缀的一部分给出了药进行运算的函数的数据范围,其中p表示封装操作,s表示变量操作,而ep表示扩展操作,接下来的部分表示要进行运算的数据类型,其中s表示单精度操作,d表示双精度操作,i表示整数操作,u表示无符号整数,数字表示整数的比特数。
所以的这些intrinsics函数可以通过Intel提供个一个软件查询,名称是:Intel_Intrinsics_Guide-windows
再来看一个例子:
下面的这段代码,由于循环体当中有一个条件语句,使用/QxP选项进行编译会发现,Intel编译器并不会进行自动矢量化。
点击(此处)折叠或打开
- #define SIZE 128
- __decspec(align(16)) short int aa[SIZE],bb[SIZE],cc[SIZE],dd[SIZE];
- void Branch_Loop(short int g)
- {
- int i;
- for(i=0;i<SIZE;i++)
- {
- aa[i] = (bb[i]>0)?(cc[i]+2):(dd[i]+g);
- }
- }
我们可以手工来实现上面的函数,通过使用SIMD指令来消除循环上的分支,同时可以一次完成8个处理,减少循环次数。__mm_cmpgt_epi16()函数可以在一次执行时完成8个比较操作,如果其中某个组元素大于0,则返回的XMM寄存器中对应的比特位全为1,否则全为0,。获得这些掩码之后就可以通过下面代码中的三条操作完成分支赋值了。
代码如下:
- #include <emmintrin.h>
- #define TOVectorAddress(x) ((__m128i *)&(x))
-
- void Branch_Loop(short int g)
- {
- __m128i a,b,c,d,mask,zero,two,g_broadcast;
- int i;
- zero = _mm_set1_epi(16);
- two = _mm_set1_epi16(2);
- g_broadcast = _mm_set1_epi16(g);
- for(i=0;i<SIZE;i+=8)
- {
- b = _mm_load_si128(ToVectorAddress(bb[i]));
- c = _mm_load_si128(ToVectorAddress(cc[i]));
- d = _mm_load_si128(ToVectorAddress(dd[i]));
- c = _mm_add_epi16(c,tw0);
- d = _mm_add_epi16(d,g_broadcast);
- maks = _mm_cmpgt_epi16(b,zero);
- //注意下面三行代码,它完成了之前代码中的分支赋值操作,从而便于软件流水执行
- a = _mm_and_si128(c,mask);
- mask = _mm_andnot_epi16(mask,d);
- a = _mm_or_si128(a,mask);
- _mm_store_si128(ToVectorAddress(aa[i]),a);
- }
- }