プログラムのパフォーマンスに対するベクトル化の影響を調査しています。これに関して、私は次のコードを書きました:
#include <stdio.h>
#include <sys/time.h>
#include <stdlib.h>
#define LEN 10000000
int main(){
struct timeval stTime, endTime;
double* a = (double*)malloc(LEN*sizeof(*a));
double* b = (double*)malloc(LEN*sizeof(*b));
double* c = (double*)malloc(LEN*sizeof(*c));
int k;
for(k = 0; k < LEN; k++){
a[k] = rand();
b[k] = rand();
}
gettimeofday(&stTime, NULL);
for(k = 0; k < LEN; k++)
c[k] = a[k] * b[k];
gettimeofday(&endTime, NULL);
FILE* fh = fopen("dump", "w");
for(k = 0; k < LEN; k++)
fprintf(fh, "c[%d] = %f\t", k, c[k]);
fclose(fh);
double timeE = (double)(endTime.tv_usec + endTime.tv_sec*1000000 - stTime.tv_usec - stTime.tv_sec*1000000);
printf("Time elapsed: %f\n", timeE);
return 0;
}
このコードでは、単純に 2 つのベクトルを初期化して乗算しています。結果は vector に保存されc
ます。私が主に興味を持っているのは、次のループをベクトル化する効果です。
for(k = 0; k < LEN; k++)
c[k] = a[k] * b[k];
次の 2 つのコマンドを使用してコードをコンパイルします。
1) icc -O2 TestSMID.c -o TestSMID -no-vec -no-simd
2) icc -O2 TestSMID.c -o TestSMID -vec-report2
2 番目のコマンドがループを正常にベクトル化するため、パフォーマンスが向上することを期待しています。しかし、私の調査によると、ループをベクトル化してもパフォーマンスは向上しません。
私はトピックにあまり詳しくないので、ここで何かを見落としているかもしれません。私のコードに何か問題がある場合はお知らせください。
よろしくお願いします。
PS: 私は Mac OSX を使用しているため、割り当てられたメモリはすべて 16 バイト アラインされているため、データをアラインする必要はありません。
編集: まず、コメントと回答をくださった皆様に感謝いたします。@Mysticial によって提案された答えについて考えましたが、ここで言及すべき点がいくつかあります。まず、@Vinska が述べたように、c[k]=a[k]*b[k]
1 サイクルしかかかりません。ループ インデックスのインクリメントとk
が よりも小さいことを確認するための比較に加えてLEN
、操作を実行するために行うべきことが他にもあります。コンパイラによって生成されたアセンブリ コードを見ると、単純な乗算には 1 サイクルよりもはるかに多くのサイクルが必要であることがわかります。ベクトル化されたバージョンは次のようになります。
L_B1.9: # Preds L_B1.8
movq %r13, %rax #25.5
andq $15, %rax #25.5
testl %eax, %eax #25.5
je L_B1.12 # Prob 50% #25.5
# LOE rbx r12 r13 r14 r15 eax
L_B1.10: # Preds L_B1.9
testb $7, %al #25.5
jne L_B1.32 # Prob 10% #25.5
# LOE rbx r12 r13 r14 r15
L_B1.11: # Preds L_B1.10
movsd (%r14), %xmm0 #26.16
movl $1, %eax #25.5
mulsd (%r15), %xmm0 #26.23
movsd %xmm0, (%r13) #26.9
# LOE rbx r12 r13 r14 r15 eax
L_B1.12: # Preds L_B1.11 L_B1.9
movl %eax, %edx #25.5
movl %eax, %eax #26.23
negl %edx #25.5
andl $1, %edx #25.5
negl %edx #25.5
addl $10000000, %edx #25.5
lea (%r15,%rax,8), %rcx #26.23
testq $15, %rcx #25.5
je L_B1.16 # Prob 60% #25.5
# LOE rdx rbx r12 r13 r14 r15 eax
L_B1.13: # Preds L_B1.12
movl %eax, %eax #25.5
# LOE rax rdx rbx r12 r13 r14 r15
L_B1.14: # Preds L_B1.14 L_B1.13
movups (%r15,%rax,8), %xmm0 #26.23
movsd (%r14,%rax,8), %xmm1 #26.16
movhpd 8(%r14,%rax,8), %xmm1 #26.16
mulpd %xmm0, %xmm1 #26.23
movntpd %xmm1, (%r13,%rax,8) #26.9
addq $2, %rax #25.5
cmpq %rdx, %rax #25.5
jb L_B1.14 # Prob 99% #25.5
jmp L_B1.20 # Prob 100% #25.5
# LOE rax rdx rbx r12 r13 r14 r15
L_B1.16: # Preds L_B1.12
movl %eax, %eax #25.5
# LOE rax rdx rbx r12 r13 r14 r15
L_B1.17: # Preds L_B1.17 L_B1.16
movsd (%r14,%rax,8), %xmm0 #26.16
movhpd 8(%r14,%rax,8), %xmm0 #26.16
mulpd (%r15,%rax,8), %xmm0 #26.23
movntpd %xmm0, (%r13,%rax,8) #26.9
addq $2, %rax #25.5
cmpq %rdx, %rax #25.5
jb L_B1.17 # Prob 99% #25.5
# LOE rax rdx rbx r12 r13 r14 r15
L_B1.18: # Preds L_B1.17
mfence #25.5
# LOE rdx rbx r12 r13 r14 r15
L_B1.19: # Preds L_B1.18
mfence #25.5
# LOE rdx rbx r12 r13 r14 r15
L_B1.20: # Preds L_B1.14 L_B1.19 L_B1.32
cmpq $10000000, %rdx #25.5
jae L_B1.24 # Prob 0% #25.5
# LOE rdx rbx r12 r13 r14 r15
L_B1.22: # Preds L_B1.20 L_B1.22
movsd (%r14,%rdx,8), %xmm0 #26.16
mulsd (%r15,%rdx,8), %xmm0 #26.23
movsd %xmm0, (%r13,%rdx,8) #26.9
incq %rdx #25.5
cmpq $10000000, %rdx #25.5
jb L_B1.22 # Prob 99% #25.5
# LOE rdx rbx r12 r13 r14 r15
L_B1.24: # Preds L_B1.22 L_B1.20
そして、ベクトル化されていないバージョンは次のとおりです。
L_B1.9: # Preds L_B1.8
xorl %eax, %eax #25.5
# LOE rbx r12 r13 r14 r15 eax
L_B1.10: # Preds L_B1.10 L_B1.9
lea (%rax,%rax), %edx #26.9
incl %eax #25.5
cmpl $5000000, %eax #25.5
movsd (%r15,%rdx,8), %xmm0 #26.16
movsd 8(%r15,%rdx,8), %xmm1 #26.16
mulsd (%r13,%rdx,8), %xmm0 #26.23
mulsd 8(%r13,%rdx,8), %xmm1 #26.23
movsd %xmm0, (%rbx,%rdx,8) #26.9
movsd %xmm1, 8(%rbx,%rdx,8) #26.9
jb L_B1.10 # Prob 99% #25.5
# LOE rbx r12 r13 r14 r15 eax
これに加えて、プロセッサは 24 バイトしかロードしません。メモリへのアクセスごとに、1 行 (64 バイト) がロードされます。さらに重要なことにa
、 、b
、および に必要なメモリc
は連続しているため、プリフェッチャーは間違いなく大いに役立ち、次のブロックを事前にロードします。そうは言っても、@Mystial で計算されたメモリ帯域幅は悲観的すぎると思います。
さらに、SIMD を使用して非常に単純な追加のプログラムのパフォーマンスを向上させる方法については、Intel Vectorization Guideに記載されています。したがって、この非常に単純なループのパフォーマンスをいくらか改善できるはずです。
Edit2: コメントありがとうございます。また、@Mysticial のサンプル コードのおかげで、SIMD によるパフォーマンス向上の効果がようやくわかりました。Mystial が述べたように、問題はメモリ帯域幅でした。a
、b
、およびc
L1 キャッシュに収まる小さいサイズを選択すると、SIMD がパフォーマンスを大幅に向上させるのに役立つことがわかります。私が得た結果は次のとおりです。
icc -O2 -o TestSMIDNoVec -no-vec TestSMID2.c: 17.34 sec
icc -O2 -o TestSMIDVecNoUnroll -vec-report2 TestSMID2.c: 9.33 sec
ループを展開すると、パフォーマンスがさらに向上します。
icc -O2 -o TestSMIDVecUnroll -vec-report2 TestSMID2.c -unroll=8: 8.6sec
また、 でコンパイルした場合、プロセッサが反復を完了するのに 1 サイクルしかかからないことにも言及する必要があります-O2
。
PS: 私のコンピューターは Macbook Pro Core i5 @2.5GHz (デュアルコア) です。