この投稿は、私が投稿した別の投稿と密接に関連しています 数日前 。今回は、要素の配列のペアを追加し、結果に別の配列の値を乗算して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%高速などのパフォーマンスを発揮します。また、単精度値でテストを行ったところ、同様の結果が得られました。その理由は何ですか?
GCCでのベクトル化は、-O3
で有効になります。そのため、-O0
では、通常のスカラーSSE2命令(movsd
、addsd
など)のみが表示されます。 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元のループの反復が並列に実行されます。
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