ベクトルのフル活用と -opt-assume-safe-padding オプションの使用
この記事は、インテル® デベロッパー・ゾーンに掲載されている「Utilizing Full Vectors and Use of Option -opt-assume-safe-padding」の日本語参考訳です。
効率良いベクトル化には、ベクトル・ハードウェアを最大限に活用することが重要です。このためには、パフォーマンスに影響するコードはピールループやリマインダー・ループではなくカーネル・ベクトル・ループで実行されるべきです。
リマインダー・ループ:
ベクトルループのトリップカウントがベクトル長の倍数でない場合、残りの反復はリマインダー・ループで処理されます。これは多くの場合避けられませんが、リマインダー・ループで大量の時間を費やすことはパフォーマンスの低下につながります。例えば、ベクトルループのトリップカウントが 20 でベクトル長が 16 の場合、カーネルループを 1 回実行するごとに、残りの 4 つの反復をリマインダー・ループで実行しなければいけません。インテル® MIC アーキテクチャー対応コンパイラーはリマインダー・ループをベクトル化しますが (-vec-report6 オプションを指定するとレポートが出力されます)、カーネルループほど効率良くありません。例えば、リマインダー・ループにはマスクを利用します。また、(メモリーフォルトが発生しないように) ユニットストライド形式のロード/ストアの代わりにギャザー/スキャッターを使用しなければいけません。これを回避する最適な対処法は、リマインダー・ループがランタイムに実行されないようにする (トリップカウントをベクトル長の倍数にする) か、トリップカウントをベクトル長よりも大きくする (リマインダー・ループでの実行のオーバーヘッドを減らす) ようにアルゴリズム/コードを再構成することです。
コンパイラーの最適化では、実際のトリップカウントの値に関するあらゆる情報が考慮されます。トリップカウントが 20 で、コンパイラーがあらかじめそれを知っている場合 (例えば、トリップカウントが静的定数の場合) のほうが、トリップカウント n (シンボリック値) がランタイムで 20 になった場合 (例えば、ファイルから入力値を読み込む場合) よりも適切な決定が下されます。後者の場合、ループの前で “#pragma loop_count (20)” (C/C++) または “CDEC$ LOOP COUNT (20)” (Fortran) プラグマ/宣言子を指定してコンパイラーにトリップカウントを知らせることができます。
また、コンパイラーによって行われたベクトルループのアンロールについても考慮してください (-vec-report6 オプションにより出力されるレポートを参照)。例えば、コンパイラーが (トリップカウント n でベクトル長 16 の) ループをベクトル化して (ベクトル化の後に) 係数 2 でループをアンロールすると、各カーネルループはオリジナルのソースループの 32 反復を実行します。動的なトリップカウントが 20 の場合、カーネルループは完全にスキップされ、すべての実行はリマインダー・ループで行われます。これが適用された場合、”#pragma nounroll” (C/C++) または “CDEC$ NOUNROLL” (Fortran) を指定してベクトルループのアンロールをオフにします (代わりにコンパイラーのヒューリスティックに影響を与える前述の loop_count プラグマを使ってもかまいません)。
コンパイラーで生成されるリマインダー・ループのベクトル化を無効にするには、ループの前で “#pragma vector novecremainder” (C/C++) または “CDEC$ vector noremainder” (Fortran) プラグマ/宣言子を指定します (これにより、このループで生成されたピールループのベクトル化も無効になります)。コンパイラーの内部オプション -mP2OPT_hpo_vec_remainder=F を利用して (コンパイル範囲のすべてのループで) リマインダー・ループのベクトル化を無効にすることもできます。このオプションは、ベクトルループのアセンブリー・コードで、行番号からベクトル・カーネル・ループを特定する場合に便利です (このオプションを利用しないと、アセンブリー・コードを慎重に確認してループを特定しなければいけません)。
ピールループ:
コンパイラーは、ループ内部のメモリーアクセスをアライメントするため、動的なピールループを生成します。ピールループは、候補のメモリーアクセスがアライメントされるまで、オリジナルのソースループの反復を処理します。ピールループのトリップカウントはベクトル長より小さいことが保証されます。この最適化により、カーネル・ベクトル・ループでは、アライメントされたロード/ストア命令が利用できます。つまり、カーネルループのパフォーマンス効率が向上します。しかし、ピールループ自体は (コンパイラーによりベクトル化されたとしても) 非効率的です (-vec-report6 オプションにより出力されるレポートを参照)。これを回避する最適な対処法は、アクセスがアライメントされ、コンパイラーがアライメント状況を知るようにアルゴリズム/コードを再構成することです。アクセスがすべてアライメントされていることをコンパイラーが知っている場合 (プログラマーがループの前で “#pragma vector aligned” を正しく指定して、ループ内部のメモリーアクセスがすべてアライメントされていることをコンパイラーが想定することを指示した場合)、コンパイラーはピールループを生成しません。
ピールループを生成するかどうか、コンパイラーの判断に影響を与える前述の loop_count プラグマを利用してもかまいません。
ソースのループの前に “#pragma vector unaligned” (C/C++) または “CDEC$ vector unaligned” (Fortran) プラグマ/宣言子を追加することで、コンパイラーが動的なピールループを生成しないように指示できます。動的なピールを (コンパイル範囲のすべてのループで) 抑止する別の方法は、コンパイラーの内部オプション -mP2OPT_vec_alignment=6 を指定することです。
vector プラグマ/宣言子と novecremainder 節 (前述) を使って、コンパイラーにより生成されるピールループのベクトル化を無効にすることができます。コンパイラーの内部オプション -mP2OPT_hpo_vec_peel=F で (コンパイルの範囲のすべてのループで) ピールループのベクトル化を無効にすることもできます。この場合でも、コンパイラーはアライメントのために動的なピールを行うことはありますが、ピールされたループはベクトル化されません。
ループのトリップカウントが小さいと予測される場合、動的なピールループを生成することは適切ではありません。コンパイラーは、アライメントのために動的なピールを行うことを決定する前に、実際のトリップカウントの値に関するあらゆる情報 (静的定数、loop count プラグマなど) を考慮します。しかし、多くの場合、コンパイラーは実際のトリップカウントの値が分かりません。この情報を伝える 1 つの方法は、ループの前で “#pragma loop_count (20)” (C/C++) または “CDEC$ LOOP COUNT (20)” (Fortran) プラグマ/宣言子を指定することです (トリップカウントが 20 の場合)。また、別の方法としては (コンパイル範囲のベクトル候補ループのすべてに適用)、コンパイラーの内部オプション -mP2OPT_hpo_vec_esttrip=<n1> (n1 はベクトル候補ループの推定トリップカウント) を利用します。n1 の値が低い場合、コンパイラーは動的なピールループの生成をスキップします。
例:
#include <stdio.h> void foo1(float *a, float *b, float *c, int n) { int i; #pragma ivdep for (i=0; i<n; i++) { a[i] *= b[i] + c[i]; } } void foo2(float *a, float *b, float *c, int n) { int i; #pragma ivdep for (i=0; i<20; i++) { a[i] *= b[i] - c[i]; } }
行 7 のループでは、コンパイラーはカーネル・ベクトル・ループ (係数 2 でベクトル化された後にアンロール)、ピールループ、およびリマインダー・ループ (どちらもベクトル化される) を生成します。
行 16 のループでは、コンパイラーはトリップカウントが定数 (20) であることを利用して、ベクトル化される (アンロールされない) カーネルループを生成します。リマインダー・ループ (4 反復) はコンパイラーにより完全にアンロールされます (ベクトル化されません)。ピールループは生成されません。
以下のコマンドで、ベクトル化レポートを生成してみます。
% icc -O2 -vec-report6 t2.c -c -mmic -inline-level=0
t2.c(8): (列 5) リマーク: ベクトル化のサポート: 参照 a にアラインされたアクセスが含まれています。
t2.c(8): (列 5) リマーク: ベクトル化のサポート: 参照 a にアラインされたアクセスが含まれています。
t2.c(8): (列 5) リマーク: ベクトル化のサポート: 参照 b にアラインされていないアクセスが含まれています。
t2.c(8): (列 5) リマーク: ベクトル化のサポート: 参照 c にアラインされたアクセスが含まれています。
t2.c(8): (列 5) リマーク: ベクトル化のサポート: ループ本体内部でアライメントされていないアクセスが使用されています。
t2.c(7): (列 3) リマーク: ベクトル化のサポート: アンロールファクターが 2 に設定されます。
t2.c(7): (列 3) リマーク: ループがベクトル化されました。
t2.c(8): (列 5) リマーク: ベクトル化のサポート: 参照 a にアラインされていないアクセスが含まれています。
t2.c(8): (列 5) リマーク: ベクトル化のサポート: 参照 a にアラインされていないアクセスが含まれています。
t2.c(8): (列 5) リマーク: ベクトル化のサポート: 参照 b にアラインされていないアクセスが含まれています。
t2.c(8): (列 5) リマーク: ベクトル化のサポート: 参照 c にアラインされていないアクセスが含まれています。
t2.c(8): (列 5) リマーク: ベクトル化のサポート: ループ本体内部でアライメントされていないアクセスが使用されています。
t2.c(7): (列 3) リマーク: ピールループがベクトル化されました。
t2.c(8): (列 5) リマーク: ベクトル化のサポート: 参照 a にアラインされたアクセスが含まれています。
t2.c(8): (列 5) リマーク: ベクトル化のサポート: 参照 a にアラインされたアクセスが含まれています。
t2.c(8): (列 5) リマーク: ベクトル化のサポート: 参照 b にアラインされていないアクセスが含まれています。
t2.c(8): (列 5) リマーク: ベクトル化のサポート: 参照 c にアラインされていないアクセスが含まれています。
t2.c(8): (列 5) リマーク: ベクトル化のサポート: 参照 a にアラインされたアクセスが含まれています。
t2.c(8): (列 5) リマーク: ベクトル化のサポート: 参照 a にアラインされたアクセスが含まれています。
t2.c(8): (列 5) リマーク: ベクトル化のサポート: 参照 b にアラインされていないアクセスが含まれています。
t2.c(8): (列 5) リマーク: ベクトル化のサポート: 参照 c にアラインされていないアクセスが含まれています。
t2.c(8): (列 5) リマーク: ベクトル化のサポート: ループ本体内部でアライメントされていないアクセスが使用されています。
t2.c(7): (列 3) リマーク: リマインダー・ループがベクトル化されました。
t2.c(17): (列 5) リマーク: ベクトル化のサポート: 参照 a にアラインされていないアクセスが含まれています。
t2.c(17): (列 5) リマーク: ベクトル化のサポート: 参照 a にアラインされていないアクセスが含まれています。
t2.c(17): (列 5) リマーク: ベクトル化のサポート: 参照 b にアラインされていないアクセスが含まれています。
t2.c(17): (列 5) リマーク: ベクトル化のサポート: 参照 c にアラインされていないアクセスが含まれています。
t2.c(17): (列 5) リマーク: ベクトル化のサポート: ループ本体内部でアライメントされていないアクセスが使用されています。
t2.c(16): (列 3) リマーク: ループがベクトル化されました。
t2.c(17): (列 5) リマーク: ベクトル化のサポート: 参照 a にアラインされていないアクセスが含まれています。
t2.c(17): (列 5) リマーク: ベクトル化のサポート: 参照 a にアラインされていないアクセスが含まれています。
t2.c(17): (列 5) リマーク: ベクトル化のサポート: 参照 b にアラインされていないアクセスが含まれています。
t2.c(17): (列 5) リマーク: ベクトル化のサポート: 参照 c にアラインされていないアクセスが含まれています。
t2.c(17): (列 5) リマーク: ベクトル化のサポート: ループ本体内部でアライメントされていないアクセスが使用されています。
t2.c(16): (列 3) リマーク: ループはベクトル化されませんでした: ベクトル化は可能ですが非効率です。
配列のサイズを増やして、-opt-assume-safe-padding オプションによりパフォーマンスを向上する:
このオプションは、変数と動的に割り当てられたメモリーがオブジェクト境界を越えてパディングされているとコンパイラーが仮定するかどうかを制御します。
-opt-assume-safe-padding オプションが指定されると、コンパイラーは変数と動的に割り当てられたメモリーがパディングされていることを仮定します。これは、コードがプログラムで指定されたオブジェクト境界を越えて (最大 64 バイト) アクセスできることを意味します。このオプションを指定すると、コンパイラーは静的および自動オブジェクトにはパディングを追加しませんが、オブジェクトがプログラムにある場合、コードがオブジェクト境界を越えて (最大 64 バイト) アクセスできることを仮定します。この仮定に対応するため、このオプションを指定する場合はプログラムの静的および自動オブジェクトのサイズを増やす必要があります。
1. このオプションが役立つ 1 つの例は、コンパイラーが生成するベクトル・リマインダー・ループとベクトル・ピール・ループのシーケンスです。このオプションを指定すると、これらのループにおけるメモリー操作のパフォーマンスが向上します。
このオプションを上記のコンパイルで指定すると、コンパイラーは配列 a、b、c が n を 64 バイト越えてパディングされると仮定します。
これらの配列が malloc を用いて次のように割り当てられていた場合、
ptr = (float *)malloc(sizeof(float) * n);
次のように変更されます。
ptr = (float *)malloc(sizeof(float) * n + 64);
このオプションの要件を満たすように変更を加えた後、このオプションを上記のコンパイルに追加すると、行 7 のループについて生成されるピールループのシーケンスは次のように (高性能に) なります。
..B2.7: # Preds ..B2.9 ..B2.6 Latency 13 vpcmpgtd %zmm0, %zmm2, %k0 #7.3 c1 nop #7.3 c5 knot %k0, %k1 #7.3 c9 jkzd ..B2.9, %k1 # Prob 20% #7.3 c13 # LOE rdx rbx rbp rsi rdi r9 r10 r11 r12 r13 r14 r15 eax ecx r8d zmm0 zmm1 zmm2 zmm3 k1 ..B2.8: # Preds ..B2.7 Latency 53 vmovaps %zmm1, %zmm4 #8.13 c1 vmovaps %zmm1, %zmm5 #8.20 c5 vmovaps %zmm1, %zmm6 #8.5 c9 vloadunpacklps (%rsi,%r10,4), %zmm4{%k1} #8.13 c13 vloadunpacklps (%rdx,%r10,4), %zmm5{%k1} #8.20 c17 vloadunpacklps (%rdi,%r10,4), %zmm6{%k1} #8.5 c21 vloadunpackhps 64(%rsi,%r10,4), %zmm4{%k1} #8.13 c25 vloadunpackhps 64(%rdx,%r10,4), %zmm5{%k1} #8.20 c29 vloadunpackhps 64(%rdi,%r10,4), %zmm6{%k1} #8.5 c33 vaddps %zmm5, %zmm4, %zmm7 #8.20 c37 vmulps %zmm7, %zmm6, %zmm8 #8.5 c41 nop #8.5 c45 vpackstorelps %zmm8, (%rdi,%r10,4){%k1} #8.5 c49 vpackstorehps %zmm8, 64(%rdi,%r10,4){%k1} #8.5 c53 movb %al, %al #8.5 c53 # LOE rdx rbx rbp rsi rdi r9 r10 r11 r12 r13 r14 r15 eax ecx r8d zmm0 zmm1 zmm2 zmm3 ..B2.9: # Preds ..B2.7 ..B2.8 Latency 9 addq $16, %r10 #7.3 c1 vpaddd %zmm3, %zmm2, %zmm2 #7.3 c5 cmpq %r11, %r10 #7.3 c5 jb ..B2.7 # Prob 82% #7.3 c9
このオプションを指定しないと、コンパイラーは行 7 のピールループにギャザー/スキャッターを使用してより性能の低いコードを生成します。
..B2.7: # Preds ..B2.9 ..B2.6 Latency 13 vpcmpgtd %zmm0, %zmm2, %k0 #7.3 c1 nop #7.3 c5 knot %k0, %k4 #7.3 c9 jkzd ..B2.9, %k4 # Prob 20% #7.3 c13 # LOE rax rdx rbx rbp rsi rdi r9 r11 r13 r15 ecx r8d r10d zmm0 zmm1 zmm2 zmm3 k4 ..B2.8: # Preds ..B2.7 Latency 57 vmovaps .L_2il0floatpacket.10(%rip), %zmm8 #8.5 c1 vmovaps %zmm1, %zmm4 #8.13 c5 lea (%rsi,%r13), %r14 #8.13 c5 vmovaps %zmm1, %zmm5 #8.20 c9 kmov %k4, %k2 #8.13 c9 ..L15: #8.13 vgatherdps (%r14,%zmm8,4), %zmm4{%k2} #8.13 jkzd ..L14, %k2 # Prob 50% #8.13 vgatherdps (%r14,%zmm8,4), %zmm4{%k2} #8.13 jknzd ..L15, %k2 # Prob 50% #8.13 ..L14: # vmovaps %zmm1, %zmm6 #8.5 c21 kmov %k4, %k3 #8.20 c21 lea (%rdx,%r13), %r14 #8.20 c25 lea (%rdi,%r13), %r12 #8.5 c25 ..L17: #8.20 vgatherdps (%r14,%zmm8,4), %zmm5{%k3} #8.20 jkzd ..L16, %k3 # Prob 50% #8.20 vgatherdps (%r14,%zmm8,4), %zmm5{%k3} #8.20 jknzd ..L17, %k3 # Prob 50% #8.20 ..L16: # vaddps %zmm5, %zmm4, %zmm7 #8.20 c37 kmov %k4, %k1 #8.5 c37 ..L19: #8.5 vgatherdps (%r12,%zmm8,4), %zmm6{%k1} #8.5 jkzd ..L18, %k1 # Prob 50% #8.5 vgatherdps (%r12,%zmm8,4), %zmm6{%k1} #8.5 jknzd ..L19, %k1 # Prob 50% #8.5 ..L18: # vmulps %zmm7, %zmm6, %zmm9 #8.5 c49 nop #8.5 c53 ..L21: #8.5 vscatterdps %zmm9, (%r12,%zmm8,4){%k4} #8.5 jkzd ..L20, %k4 # Prob 50% #8.5 vscatterdps %zmm9, (%r12,%zmm8,4){%k4} #8.5 jknzd ..L21, %k4 # Prob 50% #8.5 ..L20: # # LOE rax rdx rbx rbp rsi rdi r9 r11 r13 r15 ecx r8d r10d zmm0 zmm1 zmm2 zmm3 ..B2.9: # Preds ..B2.7 ..B2.8 Latency 9 addq $16, %rax #7.3 c1 addq $64, %r13 #7.3 c1 vpaddd %zmm3, %zmm2, %zmm2 #7.3 c5 cmpq %r9, %rax #7.3 c5 jb ..B2.7 # Prob 82% #7.3 c9
2. このオプションが便利な他の例は、短整数型変換の制御です。この場合、デフォルトオプションで生成されるコードは、-opt-assume-safe-padding オプションを追加することで高性能になります。
void foo(short * restrict a, short *restrict b, short * restrict c) { int i; for(i = 0; i < N; i++) { a[i] = b[i] + c[i]; } }
メイン・カーネル・ループで、コンパイラーは (メモリーフォルトが発生しないように) ロード/ストアのチェックを追加します。リマインダー・ループのギャザー/スキャッターは省略されます。
デフォルトオプションのメイン・カーネル・ループ:
..B1.6: lea (%rax,%rsi), %r10 vloadunpackld (%rax,%rsi){sint16}, %zmm1 andq $63, %r10 cmpq $32, %r10 jle ..L3 vloadunpackhd 64(%rax,%rsi){sint16}, %zmm1 ..L3: vprefetch1 256(%rax,%rsi) lea (%rax,%rdx), %r10 vloadunpackld (%rax,%rdx){sint16}, %zmm2 andq $63, %r10 cmpq $32, %r10 jle ..L4 vloadunpackhd 64(%rax,%rdx){sint16}, %zmm2 ..L4: vprefetch0 128(%rax,%rsi) vpaddd %zmm2, %zmm1, %zmm3 vprefetch1 256(%rax,%rdx) vpandd %zmm0, %zmm3, %zmm4 vprefetch0 128(%rax,%rdx) addq $16, %rcx vprefetch1 256(%rax,%rdi) lea (%rax,%rdi), %r10 andq $63, %r10 cmpq $32, %r10 jle ..L5 vpackstorehd %zmm4{uint16}, 64(%rax,%rdi) ..L5: vpackstoreld %zmm4{uint16}, (%rax,%rdi) vprefetch0 128(%rax,%rdi) addq $32, %rax cmpq $992, %rcx jb ..B1.6
デフォルトオプションのリマインダー・ループ:
..L9: vpgatherdd 1984(%rdx,%zmm3,2){sint16}, %zmm1{%k2} jkzd ..L8, %k2 vpgatherdd 1984(%rdx,%zmm3,2){sint16}, %zmm1{%k2} jknzd ..L9, %k2 ..L8: vpaddd %zmm1, %zmm0, %zmm2 vpandd .L_2il0floatpacket.3(%rip), %zmm2, %zmm4 nop ..L11: vpscatterdd %zmm4{uint16}, 1984(%rdi,%zmm3,2){%k3} jkzd ..L10, %k3 vpscatterdd %zmm4{uint16}, 1984(%rdi,%zmm3,2){%k3} jknzd ..L11, %k3 ..L10:
-opt-assume-safe-padding オプションを追加すると、コンパイラーはより高性能なメイン・カーネル・ループとリマインダー・ループを生成します。
-opt-assume-safe-padding オプションを追加した場合のメイン・カーネル・ループ:
..B1.6: vloadunpackld (%rax,%rsi){sint16}, %zmm1 vprefetch1 256(%rax,%rsi) vloadunpackld (%rax,%rdx){sint16}, %zmm2 vprefetch0 128(%rax,%rsi) vloadunpackhd 64(%rax,%rsi){sint16}, %zmm1 vprefetch1 256(%rax,%rdx) vloadunpackhd 64(%rax,%rdx){sint16}, %zmm2 vprefetch0 128(%rax,%rdx) vpaddd %zmm2, %zmm1, %zmm3 vprefetch1 256(%rax,%rdi) vpandd %zmm0, %zmm3, %zmm4 vprefetch0 128(%rax,%rdi) addq $16, %rcx movb %dl, %dl vpackstoreld %zmm4{uint16}, (%rax,%rdi) vpackstorehd %zmm4{uint16}, 64(%rax,%rdi) addq $32, %rax cmpq $992, %rcx jb ..B1.6
-opt-assume-safe-padding オプションを追加した場合のリマインダー・ループ (ギャザー/スキャッターのない高性能バージョン):
vloadunpackld 1984(%rsi){sint16}, %zmm0{%k1} vloadunpackld 1984(%rdx){sint16}, %zmm1{%k1} vloadunpackhd 2048(%rsi){sint16}, %zmm0{%k1} vloadunpackhd 2048(%rdx){sint16}, %zmm1{%k1} vpaddd %zmm1, %zmm0, %zmm2 vpandd .L_2il0floatpacket.3(%rip), %zmm2, %zmm3 nop vpackstoreld %zmm3{uint16}, 1984(%rdi){%k1} vpackstorehd %zmm3{uint16}, 2048(%rdi){%k1} movb %al, %al
次のステップ
この記事は、「Programming and Compiling for Intel® Many Integrated Core Architecture」(英語) の一部「Utilizing Full Vectors and Use of Option -opt-assume-safe-padding」の翻訳です。インテル® Xeon Phi™ コプロセッサー上にアプリケーションを移植し、チューニングを行うには、各リンクのトピックを参照してください。アプリケーションのパフォーマンスを最大限に引き出すために必要なステップを紹介しています。
「ベクトル化の基本」に戻る
コンパイラーの最適化に関する詳細は、最適化に関する注意事項を参照してください。