GCC SSE 代码优化
Posted
技术标签:
【中文标题】GCC SSE 代码优化【英文标题】:GCC SSE code optimization 【发布时间】:2011-12-16 16:27:38 【问题描述】:这篇文章与我发布的另一篇 some days ago 密切相关。这一次,我编写了一个简单的代码,它只是添加了一对元素数组,将结果乘以另一个数组中的值并将其存储在第四个数组中,所有变量都是浮点双精度类型。
我制作了该代码的两个版本:一个带有 SSE 指令,使用调用,另一个没有它们,然后我使用 gcc 和 -O0 优化级别编译它们。我把它们写在下面:
// SSE VERSION
#define N 10000
#define NTIMES 100000
#include <time.h>
#include <stdio.h>
#include <xmmintrin.h>
#include <pmmintrin.h>
double a[N] __attribute__((aligned(16)));
double b[N] __attribute__((aligned(16)));
double c[N] __attribute__((aligned(16)));
double r[N] __attribute__((aligned(16)));
int main(void)
int i, times;
for( times = 0; times < NTIMES; times++ )
for( i = 0; i <N; i+= 2)
__m128d mm_a = _mm_load_pd( &a[i] );
_mm_prefetch( &a[i+4], _MM_HINT_T0 );
__m128d mm_b = _mm_load_pd( &b[i] );
_mm_prefetch( &b[i+4] , _MM_HINT_T0 );
__m128d mm_c = _mm_load_pd( &c[i] );
_mm_prefetch( &c[i+4] , _MM_HINT_T0 );
__m128d mm_r;
mm_r = _mm_add_pd( mm_a, mm_b );
mm_a = _mm_mul_pd( mm_r , mm_c );
_mm_store_pd( &r[i], mm_a );
//NO SSE VERSION
//same definitions as before
int main(void)
int i, times;
for( times = 0; times < NTIMES; times++ )
for( i = 0; i < N; i++ )
r[i] = (a[i]+b[i])*c[i];
当使用 -O0 编译它们时,gcc 使用 XMM/MMX 寄存器和 SSE 指令,如果没有特别给出 -mno-sse(和其他)选项。我检查了为第二个代码生成的汇编代码,我注意到它使用了 movsd、addsd 和 mulsd 指令。所以它使用 SSE 指令,但只使用那些使用寄存器最低部分的指令,如果我没记错的话。正如预期的那样,为第一个 C 代码生成的汇编代码使用了 addp 和 mulpd 指令,尽管生成了相当大的汇编代码。
无论如何,据我所知,SIMD 范式的第一个代码应该会获得更好的收益,因为每次迭代都会计算两个结果值。尽管如此,第二个代码的执行速度比第一个代码快 25%。我还用单精度值进行了测试并得到了类似的结果。这是什么原因?
【问题讨论】:
在没有优化的情况下比较性能是毫无意义的。 您正在为 2 次算术运算执行 3 次加载和 1 次存储,因此您很可能会受到带宽限制。 删除 _mm_prefetch 调用后会发生什么?我认为他们可能会伤害你 那些预取调用确实看起来毫无用处。内部循环中的访问模式是顺序的。 (所以硬件预取器会拾取它)此外,您只提前预取一个迭代,并且您的预取指令几乎与“工作”指令一样多...... 你是对的,当移除预取调用时,性能会提高一点(不多)。我猜预取应该只在没有任何这样的访问模式时才适用。使用 O3 编译时,第一个代码的性能要好得多。 【参考方案1】:在 GCC 中的矢量化在 -O3
启用。这就是为什么在-O0
,您只能看到普通的标量 SSE2 指令(movsd
、addsd
等)。使用 GCC 4.6.1 和您的第二个示例:
#define N 10000
#define NTIMES 100000
double a[N] __attribute__ ((aligned (16)));
double b[N] __attribute__ ((aligned (16)));
double c[N] __attribute__ ((aligned (16)));
double r[N] __attribute__ ((aligned (16)));
int
main (void)
int i, times;
for (times = 0; times < NTIMES; times++)
for (i = 0; i < N; ++i)
r[i] = (a[i] + b[i]) * c[i];
return 0;
并使用gcc -S -O3 -msse2 sse.c
编译为内部循环生成以下指令,这非常好:
.L3:
movapd a(%eax), %xmm0
addpd b(%eax), %xmm0
mulpd c(%eax), %xmm0
movapd %xmm0, r(%eax)
addl $16, %eax
cmpl $80000, %eax
jne .L3
如您所见,启用矢量化后,GCC 会发出代码以并行执行 两个 循环迭代。不过,它可以改进 - 此代码使用 SSE 寄存器的低 128 位,但它可以通过启用 SSE 指令的 AVX 编码(如果机器上可用)来使用完整的 256 位 YMM 寄存器。因此,使用gcc -S -O3 -msse2 -mavx sse.c
编译相同的程序会产生内部循环:
.L3:
vmovapd a(%eax), %ymm0
vaddpd b(%eax), %ymm0, %ymm0
vmulpd c(%eax), %ymm0, %ymm0
vmovapd %ymm0, r(%eax)
addl $32, %eax
cmpl $80000, %eax
jne .L3
注意v
在每条指令前面,并且指令使用256位YMM寄存器,原循环的四次迭代并行执行。
【讨论】:
我刚刚在x86-64
上运行了这个gcc 4.7.2
,有和没有-msse2
标志 - 两者都导致相同的汇编程序输出。那么在这个平台上默认启用 sse 指令是否安全?
@lori,是的,SSE 是 x86-64 的默认设置。
请注意 gcc 4.6 的 AVX 输出不安全:如果地址不是 32B 对齐的,vmovapd ymm
将出错,但源仅要求 16B 对齐。 gcc 4.8 及更高版本正确并进行设置/清理循环以处理数组中未与 32B 对齐的部分。 With -mavx2
, the inner loop uses 16B loads with movapd / vinsertf128 for two arrays, and a 32B aligned memory operand for the 3rd src.,但对于 -march=haswell
,它会为所有数组执行 32B 未对齐的加载/存储(在其中一个对齐 32B 之后)。这是来自-mtune=haswell
设置。【参考方案2】:
我想扩展 chill's answer 并提请您注意,GCC 在向后迭代时似乎无法对 AVX 指令进行同样的智能使用。
只需将 chill 示例代码中的内部循环替换为:
for (i = N-1; i >= 0; --i)
r[i] = (a[i] + b[i]) * c[i];
带有选项 -S -O3 -mavx
的 GCC (4.8.4) 产生:
.L5:
vmovsd a+79992(%rax), %xmm0
subq $8, %rax
vaddsd b+80000(%rax), %xmm0, %xmm0
vmulsd c+80000(%rax), %xmm0, %xmm0
vmovsd %xmm0, r+80000(%rax)
cmpq $-80000, %rax
jne .L5
【讨论】:
有趣。较新的 gcc 自动矢量化,有趣的是,对每个数组输入/输出使用vpermpd 0b00011011
在加载后将其反转,因此每个向量中的数据元素按源顺序从第一个到最后一个。这是每次迭代 4 vpermpd
s!有趣的是,clang auto-vectorizes it nicely以上是关于GCC SSE 代码优化的主要内容,如果未能解决你的问题,请参考以下文章
SSE图像算法优化系列十:简单的一个肤色检测算法的SSE优化。