web-dev-qa-db-ja.com

GCC SSEコードの最適化

この投稿は、私が投稿した別の投稿と密接に関連しています 数日前 。今回は、要素の配列のペアを追加し、結果に別の配列の値を乗算して4番目の配列に格納し、すべての変数の浮動小数点倍精度型を指定する単純なコードを作成しました。

そのコードの2つのバージョンを作成しました。1つはSSE命令を使用し、呼び出しを使用し、もう1つは呼び出しを使用せず、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(およびその他)オプションが指定されていない場合)を使用します。生成されたアセンブリコードを調べました。 2番目のコードと私はそれが利用していることに気づきました movsd、 追加された そして mulsd 指示。したがって、SSE命令を使用しますが、間違いがなければ、レジスタの最下位部分を使用する命令のみを使用します。最初のCコード用に生成されたアセンブリコードは、予想どおりに使用されました。の addp そして mulpd かなり大きなアセンブリコードが生成されましたが、命令。

とにかく、最初のコードは、私が知る限り、SIMDパラダイムの利益が増えるはずです。これは、反復ごとに2つの結果値が計算されるためです。それでも、2番目のコードは最初のコードよりも25%高速などのパフォーマンスを発揮します。また、単精度値でテストを行ったところ、同様の結果が得られました。その理由は何ですか?

14
Genís

GCCでのベクトル化は、-O3で有効になります。そのため、-O0では、通常のスカラーSSE2命令(movsdaddsdなど)のみが表示されます。 GCC 4.6.1と2番目の例の使用:

#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はコードを発行してtwoループ反復を並行して実行します。ただし、このコードは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レジスタを使用することに注意してください。4元のループの反復が並列に実行されます。

15
chill

chill's answer を拡張し、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
2
Luca Citi