我学习SSE指令的初衷就是为了实现RGB<->RGBA, YUV<->RGBA、RGB,这些转换的指令优化。
在学习指令优化的过程中总是会看到SIMD(Single Instructions Multi Data), 单指令多数据:在一个指令周期内使用一条指令处理多个数据。这是Intel早期开发MMX指令就提出来的,只不过MMX指令基本是对整形数据的处理,随着时代的发展,这些功能已经不能满足浮点数处理的需求了,后续发展的SSE指令,更多的是对单精度浮点数和双精度浮点数的支持和优化,并且SSE指令扩展了数据寄存器的长度,保留了xmm0~xmm7八个128位的寄存器,可以用来存储2个双精度浮点数、4个单精度浮点数、4个整形数据、16字节的数据。
SSE指令可以分为以下几类:
1)数据移动指令:支持内存到寄存器、寄存器到内存、寄存器到寄存器的数据移动
例如:movups指令, 对128位(由4个打包的单精度浮点数组成)做上述的移动处理 __asm { float af[4] = {0, 0 ,0 ,0}; float bf[4]; movups xmm0, af; movups xmm1, xmm0; movups bf, xmm1; } movaps指令,也是对128位(由4个打包单精度浮点数组成)做上述的移动处理,不同的是,如果移动的内存如果不满128位,程序将抛出一个异常,所以movaps指令处理的内存和寄存器必须是16字节对齐的。因此上面的代码需要部分修改才能运行正常 __asm { __declspec(align(16)) af[4] = {0, 0, 0, 0}; __declspec(align(16)) af[4]; movaps xmm0 , af; movaps xmm1, xmm0; movaps bf, xmm1; } 相信大家对比movups和movaps指令就看出来了,mov表示移动,u,a分别表示不必16自己对齐和16自己对齐,而ps(packed single-precision floating-point)表示打包的单精度浮点数。对指令的构成有了初步了解之后,相信大家也很容器理解movupd和movapd的意思。 实际上不论是单精度浮点数还是双精度浮点数,数据移动更关注的是数据位是否是128位,并不关注内存中的具体数据类型,只有算术运算才会关注数据类型。 例如: __asm { float af[4] = {5.0f, 5.0f, 5.0f, 5.0f}; float bf[4]; movupd xmm0, af; movupd xmm1, xmm0; movupd bf, xmm1; } movupd 更够实现与movups一样的效果,而不出任何异常。 了解了常用的128位指令移动指令,再来看看特殊的移动指令 movsd指令,可以实现将64位内存的数据移动到寄存器的低64,将寄存器的低64位移动到内存中,以及寄存器a的低64位移动到寄存器b的低64位并保持高64位不变。 movss指令与movsd指令类似,只不过是对32位数据的移动.
还有其他的移动指令就不一一列举了,大家可以在intel指令手册中查到。
在这里多说一句,__asm是C++内联汇编的关键字,目前大多数C++编译器都支持对它支持。
2)算术运算指令:包括一般的四则运算,也有平方和开方运算,开方的倒数运算,求平均数运算。
下面写一个简单的例子,使用算术指令一次分别完成多个数据的加减乘除运算。
float af[4] = {5.0f, 6.0f, 7.0f, 8.0f}; float bf[4] = {5.0f, 6.0f, 7.0f, 8.0f}; float add[4], sub[4], mul[4], div[4];
__asm
{ movups xmm0, af; movups xmm1, bf; movups xmm2, xmm0; // 加法 addps xmm0, xmm1; movups add, xmm0; // 减法 movups xmm0, xmm2; subps xmm0, xmm1; movups sub, xmm0; // 乘法 movups xmm0, xmm2; mulps xmm0, xmm1; movups mul, xmm0; // 除法 movups xmm0, xmm2; divps xmm0, xmm1; movups div, xmm0; } // 上面用到的四则运算指令都是浮点运算指令 int ai[4] = {4, 5, 6, 7}; int bi[4] = {4,5, 6, 7}; int add[4], sub[4], mul[4], div[4];
__asm
{ movupd xmm0, ai; movupd xmm1, bi; movupd xmm2, xmm0; // 加法 paddd xmm0, xmm1; movupd add, xmm0; // 减法 movupd xmm0, xmm2; psubd xmm0, xmm1; movupd sub, xmm0; // 乘法 movupd xmm0, xmm2; pmulld xmm0, xmm1; movupd mul, xmm0; // 除法 movupd xmm0, xmm2; divps xmm0, xmm1; movupd div, xmm0; }
加法、减法、乘法,分别对应有浮点运算和整形运算指令,而除法运算只有浮点运算指令。我们都知道CPU由于只有少量的浮点运算单元,所以浮点运算的效率要远低于整形运算,而乘除法的运算效率又远低于加减运算。即使使用指令完成复杂运算的书写,也不一定能实现运算效率的提升,甚至在Release开启优化的情况下,使用指令做了太多的浮点乘法或除法,反而没有高级语言被编译器优化后的执行效率高。因此我们应该要求自己,在精度允许的情况下,尽量将浮点运算用整形运算代替,并且考虑使用移位运算代替乘法和除法运算。接下来让我们了解一下上面代码中用到的算术指令。
addps:对128位寄存器的每32位做浮点加法运算。
subps:对128位寄存器的没32位做浮点减法运算。
mulps:对128位寄存器的每32位做浮点乘法运算,并且不考虑乘法可能形成的进位。
divps:对128位寄存器的每32位做浮点除法运算。
paddd:对128位寄存器的每32位做整形加法运算。不过我在做YUV与RGB互转的指令优化中用到更多的是paddw,该指令是对128位寄存器的每16位做加法运算,在保证不出现进位的情况下,paddw指令比paddd一次能处理更多字节的数据。
psubd:对128位寄存器的每32位做整形减法运算。当然也有psubw可以处理16位整形减法。
pmulld:对128位寄存器的每32位做整形乘法运算,形成一个64位的立即数,然后取立即数的低32位到目的寄存器的对应bit位中。诸如此类的pmullw,是对128位寄存器的每16位做整形乘法运算,形成一个32位立即数,然后取立即数的低16位到目的寄存器的对应bit位中。
3)扩展压缩指令:对数据做重新排布,压缩等操作
int ai[4] = {4, 3, 4, 3}; int bi[4] = {0}; __asm { movups xmm0, ai; shufps xmm0, xmm0, 0xd8; movups bi, xmm0 // bi[4] = {4 , 4, 3, 3} // shufps是一个三操作数指令,从目的操作(一般指令的第一个操作数就是目的操作数)和源操作数中按指定的立即数取数据 // 立即数由八个二进制位组成 // 目的操作数和源操作数都是由4个单精度浮点数构成,立即数的低4位中每两位(0-3)决定取目的操作数的第几个32位数据,立即数的高4位中的每两位(0-3)决定取源操作数的第几个32位数据。 } short as[8] = {4, 0, 0, 0, 3, 0, 0, 0}; short as[8] = {4, 0 ,0, 0, 3, 0, 0, 0}; short asMaskL[8] = {0, 1, 0, 1, 0, 1, 0, 1}; int ci[4] = {0}; __asm { movups xmm0, as; movups xmm1, bs; packssdw xmm0, xmm1; // xmm0 03 04 03 04 packssdw xmm0, xmm0; // xmm0 34 34 34 34 punpcklwd xmm0, xmm0; // xmm0 33 44 33 44 shufps xmm0, xmm0, 0xd8; // xmm0 33 33 44 44 pand xmm0, asMaskL; // xmm0 30 30 40 40 movups ci, xmm0; // ci[4] = {4, 4, 3, 3}; }
shufps指令在上面的代码注释中已经写到了就不再赘述,这里在提一下我常用到的pshuflw和pshufhw,既然也有shuf,其实大家就应该想到与shufps指令类似,只是pshuflw是根据指定的立即数取目的寄存器和源寄存器的低64位,取数据的方式与shufps相似,只是每次根据2个二进制位取出一个word(16位),并且保持目的寄存器的高64位不变;pshufhw 则恰恰相反。
packssdw指令,将目的寄存器的每一个双字压缩成一个字把结果存在目的寄存器的低64位,并且把源寄存器的每一个双字压缩成一个字把结果存入目的寄存器的高64位。
packsswb指令,与packssdw类似,只是将一个字压缩成一个字节。
punpcklbw指令,将目的寄存器和源寄存器的低64位按字节交叉,将结果存入目的寄存器。
punpcklwd指令,将目的寄存器和源寄存器的低64位按字交叉,将结果存入目的寄存器。
punpckldq指令,将目的寄存器和源寄存器的低64位按双字交叉,将结果存入目的寄存器。
punpcklqdq指令,将目的寄存器和源寄存器的低64位按四字交叉,将结果存入目的寄存器。
当然也有punpckhbw, punpckhwd, punpckhdq,punpckhqdq,分别是对目的寄存器和源寄存器的高64位做交叉处理的。
写到这里,觉得是是否暂停一下,了解了这些常用的指令,就可以做我接下来真正要做的工作了。
因此我接下来会写一写YUV转RGB的指令优化。
最后给出intel各种指令集的网址
https://software.intel.com/sites/landingpage/IntrinsicsGuide/