1.1.1 算术运算

SSE内置函数中支持的算术操作如表1-1所示。从表中可以看出,SSE几乎支持所有常用的算术运算。故可以预知:大多数算法只要满足向量化数据并行的特点,就有可能能够使用SSE指令进行向量化。

表1-1 SSE算术运算

表1-1中有一些不太常见的指令,本节简要介绍一些。

hadd表示将一个向量相邻的两个元素相加,并要保持原来的向量大小。因此它具有两个参数,如下例所示:


__m128 _mm_hadd_ps(__m128 a, __m128 b);
r[0] = a[0]+a[1];
r[1] = a[2]+a[3];
r[2] = b[0]+b[1];
r[3] = b[2]+b[3];
__m128 _mm_addsub_ps(__m128 a, __m128 b);
r[0] = a[0]-a[1];
r[1] = a[2]+a[3];
r[2] = b[0]-b[1];
r[3] = b[2]+b[3];

其中:r表示结果,使用数组表示法就是表示向量中的第几个元素,如[2]表示其为向量寄存器中保存的第三个元素。

使用hadd能够比较容易地实现多个向量求内积,如代码清单1-1所示。

代码清单1-1 使用hadd求向量内积


__m128 a = _mm_mul_ps(b, c);
__m128 zero = _mm_setzero_ps();
a = _mm_hadd_ps(a, zero);
a = _mm_hadd_ps(a, zero);

运算完成后,a中的第一个元素即为b、c两个向量的内积。

其实addsub应该写作subadd,因为第一个操作是减法。hsub和hadd类似,只是执行的运算是减法。

比较有意思的是dp操作,对于单精度浮点类型的数据,其定义如下:


__m128 _mm_dp_ps(__m128 a, __m128 b, const int mask);
__m128 tmp;
tmp[0] = (mask[4]==1) ? (a[0]*b[0]) : 0.0;
tmp[1] = (mask[5]==1) ? (a[1]*b[1]) : 0.0;
tmp[2] = (mask[6]==1) ? (a[2]*b[2]) : 0.0;
tmp[3] = (mask[7]==1) ? (a[3]*b[3]) : 0.0;
__mm32 tmp4
tmp4 = tmp[0] + tmp[1] + tmp[2] + tmp[3];
r[0] = (mask[0]==1) ? tmp4 : 0.0;
r[1] = (mask[1]==1) ? tmp4 : 0.0;
r[2] = (mask[2]==1) ? tmp4 : 0.0;
r[3] = (mask[3]==1) ? tmp4 : 0.0;

很明显,一条dp操作可以直接实现两个向量的内积运算,如代码清单1-2所示。

代码清单1-2 使用dp操作实现两个向量的内积运算


int mask = 1+(1<<4)+(1<<5)+(1<<6)+(1<<7);
__m128 r = _mm_dp_ps(b, c, mask);

blend和blendv操作类似于C/C++中的三目运算符(?:),blend依据掩码的位选择,而blendv依据掩码向量中各元素的符号位选择,示例如下:


__m128 _mm_blend_ps(__m128 a, __m128 b, const int mask);
r[0] = (mask[0]==0) ? a[0] : b[0];
r[1] = (mask[1]==0) ? a[1] : b[1];
r[2] = (mask[2]==0) ? a[2] : b[2];
r[3] = (mask[3]==0) ? a[3] : b[3];
__m128 _mm_blendv_ps(__m128 a, __m128 b, __m128 mask);
r[0] = (mask[0]&0x80000000) ? b[0] : a[0];
r[1] = (mask[1]&0x80000000) ? b[1] : a[1];
r[2] = (mask[2]&0x80000000) ? b[2] : a[2];
r[3] = (mask[3]&0x80000000) ? b[3] : a[3];

很容易想到,可以使用blendv向量化一些简单的分支运算,如代码清单1-3所示。

代码清单1-3 简单的可向量化的分支运算示例


for(int i = 0; i < n; i++){
        r[i] = a[i] > 0 ? b[i]:c[i];
}

这同时要求许多比较指令返回结果的符号位为0或1。