如何使用SSE指令提高FIR算法效率
在搭建好VC6.0 SSE环境后(请参考:如何使VC6支持内嵌SSE指令),菜菜这里就开始现学现卖了J。因为自己的机器较老,仅支持MMX/SSE/SSE2指令,所以这里的方法也仅仅局限于此。
好的,言归正传!一般的FIR算法如下:
static void ShowFIR( float *in, float *out, float *coeff, unsigned int count ) { unsigned int i; unsigned int j;
for ( i = 0; i < count - TAP; ++ i ) { float sum;
sum = 0; for ( j = 0; j < TAP; ++ j ) { sum += in[ j ] * coeff[ j ]; } *out ++ = sum; ++ in; } } |
那么如何使用SSE指令来优化它呢?
SSE的指令在于它使用了向量计算,极大的提高了运算速度,所以在优化之前,我们首先要对原有的c代码进行向量构造。因为水平有限,构造了一个粗糙向量(如果有那位网友能够指导我进行优化,我将万分感谢!)。
这里假设相关的所有长度都是16个字节的倍数。
static void ShowFIR_O1( float *in, float *out, float *coeff, unsigned int count ) { unsigned int i; unsigned int j;
for ( i = 0; i < count - TAP; i += 4 ) { out[ 0 ] = 0; out[ 1 ] = 0; out[ 2 ] = 0; out[ 3 ] = 0; for ( j = 0; j < TAP; j += 4 ) { out[ 0 ] += in[ j ] * coeff[ j ]; out[ 0 ] += in[ j + 1 ] * coeff[ j + 1 ]; out[ 0 ] += in[ j + 2 ] * coeff[ j + 2 ]; out[ 0 ] += in[ j + 3 ] * coeff[ j + 3 ];
out[ 1 ] += in[ j + 1 ] * coeff[ j ]; out[ 1 ] += in[ j + 2 ] * coeff[ j + 1 ]; out[ 1 ] += in[ j + 3 ] * coeff[ j + 2 ]; out[ 1 ] += in[ j + 4 ] * coeff[ j + 3 ];
out[ 2 ] += in[ j + 2 ] * coeff[ j ]; out[ 2 ] += in[ j + 3 ] * coeff[ j + 1 ]; out[ 2 ] += in[ j + 4 ] * coeff[ j + 2 ]; out[ 2 ] += in[ j + 5 ] * coeff[ j + 3 ];
out[ 3 ] += in[ j + 3 ] * coeff[ j ]; out[ 3 ] += in[ j + 4 ] * coeff[ j + 1 ]; out[ 3 ] += in[ j + 5 ] * coeff[ j + 2 ]; out[ 3 ] += in[ j + 6 ] * coeff[ j + 3 ]; }
in += 4; out += 4; } } |
上面方法的主要思路是,在保持原有算法的基础上,对代码进行展开和重组,形成若干个向量组。在这个基础上,我们再使用SSE指令进行快速的矢量计算以获取高性能。从上的构建可以看出,我们有两组向量,分别用in和coeff指向。in指向的向量in[j]是16个字节地址对齐,而in[j+1], in[j+2], in[j+2]是不对齐的,故除了第一组能够使用movaps之外,其他都使用movups来装载。coeff仅使用一组向量和in的4组向量进行相乘,所以可以考虑将它放入xmm寄存器中提高运行效率。从上面的c代码可以看出,对于每组向量计算的结果将被放在4个单元中([127,96], [95,64],[63:32], [31:0]),所以在计算最后要将它们水平相加后再放入out数组中。
相关的汇编代码如下:
static void ShowFIR_O2( 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
movhlps xmm5, xmm0 addps xmm5, xmm0 movaps xmm6, xmm5 shufps xmm5, xmm5, 1 addps xmm5, xmm6 movss XMMWORD PTR[ edx + eax * 4 ], xmm5
movhlps xmm5, xmm1 addps xmm5, xmm1 movaps xmm6, xmm5 shufps xmm5, xmm5, 1 addps xmm5, xmm6 movss XMMWORD PTR[ edx + eax * 4 + 4 ], xmm5
movhlps xmm5, xmm2 addps xmm5, xmm2 movaps xmm6, xmm5 shufps xmm5, xmm5, 1 addps xmm5, xmm6 movss XMMWORD PTR[ edx + eax * 4 + 8 ], xmm5
movhlps xmm5, xmm3 addps xmm5, xmm3 movaps xmm6, xmm5 shufps xmm5, xmm5, 1 addps xmm5, xmm6 movss XMMWORD PTR[ edx + eax * 4 + 12 ], xmm5
add eax, 4 b2: cmp eax, count - TAP jb b1 } } |
为了防止与汇编保留字冲突,我将原有的C代码中的部分变量进行了重新命名。此外,汇编代码假设他们的首地址都是16字节对齐的。16字节地址对齐的方法很简单,只要在变相声明的前面加上“__declspec( align( 16 ) )”修饰,比如:
static __declspec( align( 16 ) ) float fin[ COUNT ]; static __declspec( align( 16 ) ) float fout1[ COUNT ]; static __declspec( align( 16 ) ) float fout2[ COUNT ]; static __declspec( align( 16 ) ) float fcoeff[ TAP ]; |
它告诉编译器,上面声明的变量必须16字节地址对齐J。
小结:
上面的汇编代码有几个较大的缺陷:
1. 没有对水平相加进行优化,SSE3有一个HADDPS的水平相加指令,它对性能的提升有重大作用,因为我现在无法调试(计算机仅仅支持SSE2)所以使用了上面一对难看的指令。
2. 没有对out向量进行一次向量写操作(在这个实现中,用了很傻的4次标量写操作)。主要原因是还没有充分理解所有相关的SSE指令,并将它们融会贯通。
3. 对原有代码的向量构造不够好,我想一定有一种方法可以避免最后的差劲的水平向量加操作和4次标量写操作。我的理想状态是没有水平向量相加和一次向量写操作。
我想,随着学习的深入,这些方面将会被逐一解决。我将在后继的文章中对这些遗憾的方面进行逐步探讨以获取更好的性能。如果有大侠能够出手相救,给我醍醐灌顶的提示,小弟在此叩拜了J!