如何使用SSE指令提高FIR算法效率(进化一)
在如何使用SSE指令提高FIR算法效率一文中,我对FIR进行了初步的优化(建立在SSE2指令基础上)。文中提供了两种优化方法,一种是C语言层面上的优化(请参考ShowFIR_O1实现),另外一种是SSE2指令上的优化(请参考ShowFIR_O2).正如文中最后说明,因为笔者初学SSE指令,在ShowFIR_O2中使用了4次标量内存写操作并且未对水平方向加法进行合适的操作,从而影响了算法的效率。
这里我将着力对此进行讨论,希望能够给出一个相对合理的优化方案。在讨论之前,我们现在一下ShowFIR_O2算法中的水平加法,其中字母表示对应的32位浮点数。
矩阵1 A3, A2, A1, A0 B3, B2, B1, B0 C3, C2, C1, C0 D3, D2, D1, D0 | 矩阵2 D0, C0, B0, A0 D1, C1, B1, A1 D2, C2, B2, A2 D3, D2, D1, D0 |
因为SSE2中没有对应的水平相加指令,比如HADDPS等,对于水平方面的操作相对比较困难。如果我们能够把矩阵1,变化为矩阵2,那么行向量的加法运算就能够被列向量替换(它在SSE2中很容易计算)。此外,它的另外一个好处在于我们能够通过一条向量存储指令来完成以前的4条标量存储指令。从这个角度上看,它的价值还是很显然的。
那么,现在的问题就转变成为如何使矩阵1变成矩阵2. 呵呵,此时SHUFFS就开始粉墨登场了。它对于数据块的搬动是相当灵活的,爱死它了J!
现在看看它是这么做的吧,在看晦涩的汇编代码之前,我们先了解一下矩阵1的存放。
xmm0= A3, A2, A1, A0
xmm1= B3, B2, B1, B0
xmm2= C3, C2, C1, C0
xmm3= D3, D2, D1, D0
算法将它们转变为:
xmm4= D0, C0, B0, A0
xmm5= D1, C1, B1, A1
xmm6= D2, C2, B2, A2
xmm7= D3, D2, D1, D0
因为xmm寄存器的限制,xmm2中的数据会在转化过程中被冲掉,这点需要注意。其他xmm寄存器保持矩阵1原有数据。如果网友希望xmm2保护原有数据,那么请在使用它之前备份到内存吧L! 在完成矢量相加后,我们使用movaps一条指令搞定内存写操作。恩…,很舒服,不是吗?!
movaps xmm4, xmm0 shufps xmm4, xmm1, 68 shufps xmm4, xmm4, 216 shufps xmm4, xmm2, 68 movaps xmm5, xmm2 shufps xmm5, xmm3, 68 shufps xmm4, xmm5, 132
movaps xmm5, xmm1 shufps xmm5, xmm1, 153 shufps xmm5, xmm5, 136 shufps xmm5, xmm2, 148 movaps xmm6, xmm2 shufps xmm6, xmm3, 153 shufps xmm5, xmm6, 132
movaps xmm6, xmm2 shufps xmm6, xmm1, 238 shufps xmm6, xmm6, 136 shufps xmm6, xmm2, 228 movaps xmm7, xmm2 shufps xmm7, xmm3, 238 shufps xmm6, xmm7, 132
movaps xmm7, xmm0 shufps xmm7, xmm1, 187 shufps xmm7, xmm7, 136 shufps xmm7, xmm2, 244 shufps xmm2, xmm3, 187 shufps xmm7, xmm2, 132
addps xmm4, xmm5 addps xmm4, xmm6 addps xmm4, xmm7 movaps XMMWORD PTR[ edx + eax * 4 ], xmm4 |
为了便于和以前的代码相比较,我新建了一个对应的函数,如下:
static void ShowFIR_O3( float *inPtr, float *outPtr, float *coeffPtr, unsigned int count ) { __asm { xorps xmm0, xmm0 xorps xmm1, xmm1 xorps xmm2, xmm2 xorps xmm3, xmm3 xor eax, eax xor ecx, ecx mov ebx, DWORD PTR[ coeffPtr ] mov esi, DWORD PTR[ inPtr ] mov edx, DWORD PTR[ outPtr ] jmp b2 b1: movaps xmm4, XMMWORD PTR[ ebx + ecx * 4 ]
movaps xmm5, XMMWORD PTR[ esi + ecx * 4 ] mulps xmm5, xmm4 addps xmm0, xmm5
movups xmm5, XMMWORD PTR[ esi + ecx * 4 + 4 ] mulps xmm5, xmm4 addps xmm1, xmm5
movups xmm5, XMMWORD PTR[ esi + ecx * 4 + 8 ] mulps xmm5, xmm4 addps xmm2, xmm5
movups xmm5, XMMWORD PTR[ esi + ecx * 4 + 12 ] mulps xmm5, xmm4 addps xmm3, xmm5
add ecx, 4 cmp ecx, TAP jb b1
movaps xmm4, xmm0 shufps xmm4, xmm1, 68 shufps xmm4, xmm4, 216 shufps xmm4, xmm2, 68 movaps xmm5, xmm2 shufps xmm5, xmm3, 68 shufps xmm4, xmm5, 132
movaps xmm5, xmm1 shufps xmm5, xmm1, 153 shufps xmm5, xmm5, 136 shufps xmm5, xmm2, 148 movaps xmm6, xmm2 shufps xmm6, xmm3, 153 shufps xmm5, xmm6, 132
movaps xmm6, xmm2 shufps xmm6, xmm1, 238 shufps xmm6, xmm6, 136 shufps xmm6, xmm2, 228 movaps xmm7, xmm2 shufps xmm7, xmm3, 238 shufps xmm6, xmm7, 132
movaps xmm7, xmm0 shufps xmm7, xmm1, 187 shufps xmm7, xmm7, 136 shufps xmm7, xmm2, 244 shufps xmm2, xmm3, 187 shufps xmm7, xmm2, 132
addps xmm4, xmm5 addps xmm4, xmm6 addps xmm4, xmm7 movaps XMMWORD PTR[ edx + eax * 4 ], xmm4
add eax, 4 b2: cmp eax, count - TAP jb b1 } } |
相比原来的ShowFIR_O2代码,这里多了一堆的烂东西,那么如何获知它的效率提升呢?因为到现在位置,我们仅仅是主观感觉速度会快,但是没有具体的标尺。有道理,那么我们做一个简单的试验吧!QueryPerformanceCounter()函数被用户获取调用前后的时间点,然后计算delta值来评估。下面的数据来源于对它们的10000次调用:
1: ShowFIR delta = 55895045
2: ShowFIR_O1 delta = 30832254
3: ShowFIR_O2 delta = 2003
4: ShowFIR_O3 delta = 1474
如果以ShowFIR为基准点,那么
1. ShowFIR_O1提升为:44.84%
2. ShowFIR_O2提升为:99.9964%
3. ShowFIR_O3提升为:99.9973%,相对于ShowFIR_O2而言,提升为:26.4%
小结:
1.对于SSE指令的使用,我有些被愕然!因为提升的空间太大了,有些不可思议!所以在日后的程序优化中,我们尽量在关键段中加入SSE指令以提高性能(不考虑平台的话)。
2.从语言层面上看,适当的调整代码也会获取很大的性能提升。ShowFIR_O1就是一个很好的例子,通过适当的变化,它的效能竟然也能够提升近50%!
总之,为了兼顾性能和移植,不妨先在语言层面上进行优化,如果性能依然不能符合要求,那么就让SSE出场吧(老大来了,随与争锋!)!J