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(和其他)选项。我检查了为第二个代码生成的汇编代码,我注意到它使用了 movsdaddsdmulsd 指令。所以它使用 SSE 指令,但只使用那些使用寄存器最低部分的指令,如果我没记错的话。正如预期的那样,为第一个 C 代码生成的汇编代码使用了 addpmulpd 指令,尽管生成了相当大的汇编代码。

无论如何,据我所知,SIMD 范式的第一个代码应该会获得更好的收益,因为每次迭代都会计算两个结果值。尽管如此,第二个代码的执行速度比第一个代码快 25%。我还用单精度值进行了测试并得到了类似的结果。这是什么原因?

【问题讨论】:

在没有优化的情况下比较性能是毫无意义的。 您正在为 2 次算术运算执行 3 次加载和 1 次存储,因此您很可能会受到带宽限制。 删除 _mm_prefetch 调用后会发生什么?我认为他们可能会伤害你 那些预取调用确实看起来毫无用处。内部循环中的访问模式是顺序的。 (所以硬件预取器会拾取它)此外,您只提前预取一个迭代,并且您的预取指令几乎与“工作”指令一样多...... 你是对的,当移除预取调用时,性能会提高一点(不多)。我猜预取应该只在没有任何这样的访问模式时才适用。使用 O3 编译时,第一个代码的性能要好得多。 【参考方案1】:

在 GCC 中的矢量化在 -O3 启用。这就是为什么在-O0,您只能看到普通的标量 SSE2 指令(movsdaddsd 等)。使用 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 vpermpds!有趣的是,clang auto-vectorizes it nicely

以上是关于GCC SSE 代码优化的主要内容,如果未能解决你的问题,请参考以下文章

SSE 内在函数优化

SSE图像算法优化系列十:简单的一个肤色检测算法的SSE优化。

SSE 优化(行重新排序、操作整理)中的编译器(例如 g++)有多聪明

GCC SSE 手写与生成

Delphi中的SSE2优化?

GCC编译器中的性能优化