16

这篇文章与我几天前发布的另一篇文章密切相关。这一次,我编写了一个简单的代码,它只是添加了一对元素数组,将结果乘以另一个数组中的值并将其存储在第四个数组中,所有变量都是浮点双精度类型。

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

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

4

2 回答 2

17

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 位,但它可以使用完整的 256 位 YMM 寄存器,通过启用 SSE 指令的 AVX 编码(如果在机器上可用)。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 寄存器,原始循环的四次迭代是并行执行的。

于 2011-11-10T13:18:41.913 回答
3

我想扩展寒意的答案,并提请您注意这样一个事实,即 GCC 在向后迭代时似乎无法对 AVX 指令进行同样的智能使用。

只需将 chill 示例代码中的内部循环替换为:

for (i = N-1; i >= 0; --i)
    r[i] = (a[i] + b[i]) * c[i];

带有选项的 GCC (4.8.4)-S -O3 -mavx产生:

.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
于 2016-04-21T14:42:22.770 回答