如今,我们使用的CPU指令集越来越多,功能越来越丰富。现在的CPU中,无论是Intel还是 AMD,都早已支持多媒体指令集SSE和SSE2,虽然现在这种操作都由显卡完成,但有些场合,我们还是希望使用CPU来完成一些相应的计算工作。我们使用SSE指令集可以直接使用汇编语言,也可以使用一套指令集函数,我们今天要说的是使用函数的问题。然而,网上能够找到的教程都是牛人写的,技术水平很高,对于一些涉及基础知识的部分一律省略不谈,这让很多新手朋友一头雾水不知道从哪里入手,那么这里,本人作为一个非专业的超级新手,将自己的学习心得与大家分享,希望能够给一些新手朋友一些帮助。
今天我要说的是SSE指令集,这应该是最基础的128位SIMD运算技巧,作用是使用一个指令来同时操作多个数值的运算。那么我们来看看这个SSE都包括什么。
SSE指令集大体分为三部分:
内存操作:将数值加载进入SSE专用的变量中,或者从专用的变量写入到其他格式的变量中
数值运算:加减乘除等算术运算
操作数值:变量中各部分的移动拷贝等
首先第一个部分就是让人很头疼的,因为这个东西很基础,没有教程来介绍这个部分。其实这个部分的关键是在于专用的变量类型,如:
__m128
这是个union数据类型,里面可以被各种类型共用。这个类型有128位,但这并不代表它只表示一个128位的数。实际上,这128位是要分几个部分的,如果是32位整数,可以同时容纳4组,单精度浮点也是4组,但双精度就只能是2组。由于这128位是要分组的,所以对于大家来说,如何把数据加载进去是个问题。但研究了m128的结构就会发现,加载操作数只是在方法上有些特别。一般我们加载一个SIMD变量有2种方法,一是使用命令函数来加载,二是直接对变量各部分赋值。
下面来看一个例子,最典型的是4D向量和4x4矩阵相乘:
D3DXVECTOR4 XVecMatMultiply(D3DXVECTOR4* pVec, D3DXMATRIX* pMat)
{
//load the vector into 4 XMM variables to fill up a grid of:
//xxxx
//yyyy
//zzzz
//wwww
//because each element in the vector times each element in a column of the matrix
//they repeat 4 times for 4 rows
//arrays are preferred to structures
//shuffle is also useful to fill up 4 words with specific values:
//__m128 u1 = _mm_shuffle_ps(v, v, _MM_SHUFFLE(0, 0, 0, 0));//fill up 4 words in v with the value of 1st word
__m128 u1 = _mm_load1_ps(&(pVec->x));//load1 means load 1 element into 4 words
__m128 u2 = _mm_load1_ps(&(pVec->y));
__m128 u3 = _mm_load1_ps(&(pVec->z));
__m128 u4 = _mm_load1_ps(&(pVec->w));
//convert matrix into 4 arrays
float mf1[] = { pMat->_11 , pMat->_12 ,pMat->_13 ,pMat->_14 };
float mf2[] = { pMat->_21 , pMat->_22 ,pMat->_23 ,pMat->_24 };
float mf3[] = { pMat->_31 , pMat->_32 ,pMat->_33 ,pMat->_34 };
float mf4[] = { pMat->_41 , pMat->_42 ,pMat->_43 ,pMat->_44 };
//load 4 rows into 4 XMM variables
__m128 m[4];
m[0] = _mm_load_ps(mf1);
m[1] = _mm_load_ps(mf2);
m[2] = _mm_load_ps(mf3);
m[3] = _mm_load_ps(mf4);
//transpose the matrix if loaded c by r
//_MM_TRANSPOSE4_PS(m[0],m[1],m[2],m[3]);
__m128 prod1 = _mm_mul_ps(u1, m[0]);//prod1 = x * _11, x * _12, x * _13, x * _14
__m128 prod2 = _mm_mul_ps(u2, m[1]);//prod2 = y * _21, y * _22, y * _23, y * _24
__m128 prod3 = _mm_mul_ps(u3, m[2]);//prod2 = z * _31, z * _32, z * _33, z * _34
__m128 prod4 = _mm_mul_ps(u4, m[3]);//prod2 = w * _41, w * _42, w * _43, w * _44
__m128 r = _mm_add_ps(_mm_add_ps(prod1, prod2), _mm_add_ps(prod3, prod4));//add the products togeher:
//r0 = x * _11 + y * _21 + z * _31 + w * _41
//r1 = x * _12 + y * _22 + z * _32 + w * _42
//r2 = x * _13 + y * _23 + z * _33 + w * _43
//r3 = x * _14 + y * _24 + z * _34 + w * _44
//which are just the products to be added together for each element in a vector
//store the result, note that _mm_store_ps needs an array
pVec->x = r.m128_f32[0];//the XMM variables are unions of 128 bits. Each word takes up 32 bits
pVec->y = r.m128_f32[1];
pVec->z = r.m128_f32[2];
pVec->w = r.m128_f32[3];
return *pVec;
}
在这段代码当中,一个不便的想法是传递给这个函数2个结构体变量。SSE函数更喜欢的是数组,这样可以方便地16位对齐,所以,如果要使用SSE,应该尽量使用数组。加载数值,可以用_mm_load1_ps将一个数同时写入四个分量中,也可以利用_mm_shuffle_ps的位移操作来从其他变量中选取数值,如:
u1 = _mm_shuffle_ps(v,v, _MM_SHUFFLE(0,0,0,0));
这里u1的值全部为v中的第一个元素。
算术操作会将两组__m128内的元素分别相乘并存储到变量中。
如果直接通过成员变量来进行读写操作也可,这在一些不太方便的场合适用。
这里可以看到变量r中的m128_f32是一个数组,含有4个float单精度浮点数。