SIMD

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和自动矢量化。我们用下面的求一个整数数组的和作为例子:

  1. int SumAarray(int *buf,int N)
  2. {
  3. int i,sum=0;
  4. for(i=0;i<N;i++)
  5.    sum+=buf[i];

  6. return sum;
  7. }



方法一:自动矢量化
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)的数据类型,使用时需要包含如下头文件:

  1. #include <ivec.h>//MMX
  2. #include <fvec.h>//SSE(also include ivec.h)
  3. #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++类的实现。

点击(此处)折叠或打开

  1. #include
  2. int SumAarray(int *buf,int N)
  3. {
  4. int i;
  5. I32vec4 *vec4 = (I32vec4 *)buf;
  6. I32vec4 sum(0,0,0,0);
  7. for(i=0;i<N;i++)
  8.  sum += vec4[i];
  9.  return sum[0]+sum[1]+sum[2]+sum[3];
  10. }

方法三:使用intrinsics
SIMD intrinsics有些类似于C语言中的函数,可以被其它的代码直接调用。可以像其它函数调用一样给它传递参数,Intel C++编译器支持SIMD intrinsics,并且可以针对intrinsics函数进行内联等优化。intrinsics能够被直接映射到一条或者多条SIMD指令以及其它汇编指令。至于寄存器分配、指令调度和寻址模式,则留给编译器处理。因此,相比汇编语言来说更容易使用。但是,对于生成指令的控制能力则更小。为了使用与SIMD技术相关的intrinsics,首先需要包含那些定义了数据类型和函数的头文件。

  1. #include <mmintrin.h>//mmx
  2. #include <xmmintrin.h>//sse
  3. #include <emmintrin.h>//sse2
  4. #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的边界;

  1. int SumArray(int *buf,int N)
  2. {
  3.  int i;
  4. __m128i *vec128 = (__m128i *)buf;
  5. __m128 sum;
  6. sum = _mm_sub_epi32(sum,sum);
  7. for(i=0;i<N/4;i++)
  8.    sum = _mm_add_epi32(sum,vec128[i]);
  9. sum = _mm_add_epi32(sum,_mm_srli_si128(sum,8));
  10. sum = _mm_add_epi32(sum,_mm_srli_si128(sum,4));
  11. return _mm_cvtsi128_si32(sum);
  12. }
  13. }


我们再来看一段代码,注意在循环体有一个条件语句,使用/QxP选项进行编译会发现Intel编译器并不会进行自动矢量化。

点击(此处)折叠或打开

  1. #define SIZE 128
  2. __decspec(align(16)) short int aa[SIZE],bb[SIZE],cc[SIZE],dd[SIZE];
  3. void Branch_Loop(short int g)
  4. {
  5.   int i;
  6.    for(i=0;i<SIZE;i++)
  7.    {
  8.      aa[i] = (bb[i]>0)?(cc[i]+2):(dd[i]+g);
  9.    }
  10. }
我们可以手工来实现上面的函数,通过使用SIMD指令来消除循环上的分支,同时可以一次完成8个处理,减少循环次数。__mm_cmpgt_epi16()函数可以在一次执行时完成8个比较操作,如果其中某个组元素大于0,则返回的XMM寄存器中对应的比特位全为1,否则全为0,。获得这些掩码之后就可以通过下面代码中的三条操作完成分支赋值了。
代码如下:

点击(此处)折叠或打开

  1. #include <emmintrin.h>
  2. #define TOVectorAddress(x) ((__m128i *)&(x))

  3. void Branch_Loop(short int g)
  4. {
  5.   __m128i a,b,c,d,mask,zero,two,g_broadcast;
  6.   int i;
  7.  zero = _mm_set1_epi(16);
  8.  two = _mm_set1_epi16(2);
  9.  g_broadcast = _mm_set1_epi16(g);
  10.  for(i=0;i<SIZE;i+=8)
  11.  {
  12.   b = _mm_load_si128(ToVectorAddress(bb[i]));
  13.   c = _mm_load_si128(ToVectorAddress(cc[i]));
  14.   d = _mm_load_si128(ToVectorAddress(dd[i]));
  15.   c = _mm_add_epi16(c,tw0);
  16.   d = _mm_add_epi16(d,g_broadcast);
  17.   maks = _mm_cmpgt_epi16(b,zero);
  18.  //注意下面三行代码,它完成了之前代码中的分支赋值操作,从而便于软件流水执行
  19.   a = _mm_and_si128(c,mask);
  20.   mask = _mm_andnot_epi16(mask,d);
  21.   a = _mm_or_si128(a,mask);
  22.    _mm_store_si128(ToVectorAddress(aa[i]),a);
  23.  }
  24. }

你可能感兴趣的:(并行计算)