インテル® コンパイラーによる AVX 最適化入門: 第3回 AVX への最適化について(その2)

同カテゴリーの次の記事

インテル® コンパイラーによる AVX 最適化入門: 第4回 AVX への最適化について(その3)

このミニ連載では、インテル® AVX拡張命令セットを利用した最適化について、4 回に分けて説明します。コンパイラー最適化入門第1回で紹介されている通り、AVX は SSE 系命令の流れを汲む SIMD 命令であり、第 2 世代インテル® Core™ i7 プロセッサー・ファミリー (2011 年発売) で最初に実装されました。インストール・ベースが着実に伸びている AVX に対して、どのような最適化が有効であるのか、要点をかいつまんで解説していきます。

第1回 第2回 第3回 最終回

第3回 AVX への最適化について(その2)

インテル® コンパイラーによる AVX 最適化入門の第 3 回は、第 2 世代インテル® Core™ プロセッサー・ファミリーの開発チーム拠点のある地中海の港町ハイファにて筆を取りました。当初 3 回の連載予定でしたが、第 4 回へ続きます。

①   __assume()を使った最適化

__assume() を使うことによりコンパイラーの最適化に有用な情報を伝えることが可能です。以下の例では変数 num1 が 8 の倍数である(具体的な値は問わない)と指示することで、b[0], b[0+num1], b[0-num1] が同じ 32 バイト境界のアドレスにあることがコンパイラーに伝わります。

void foo(float *restrict a, float *b, int n, int num1){
   int i;
   __assume_aligned(a, 32);
   __assume_aligned(b, 32);
   __assume(num1%8==0);
   for (i=0; i<n; i++){
      a[i] += b[i]+b[i+num1]+b[i-num1];
   }
}

②   ループの反復回数についてのヒント

ループの反復回数は変数で与えられることが多く、その場合でもコンパイラーは八方手を尽くして何とか手がかりを得ようと努力するわけですが、最終的には「あてずっぽうに決め打ち」しなければならないことが頻繁に起こります。

ループの反復回数は自動ベクトル化の性能のモデル化(たとえば、128 ビットの AVX コードを生成するか、256 ビットの AVX コードを生成するか、ベクトル化を断念するかなどを決定)において重要な役割を果たしているので、インテル® コンパイラーでは loop count というプラグマをサポートしてプログラマーからのヒントを取り入れています。詳しくはプラグマのマニュアル (英語)をご参照ください。インテル® コンパイラー マニュアルの日本語版は、エクセルソフト株式会社のサイトより登録を行うとダウンロードできます。

以下の例ではループの反復回数が平均で 10 万回見込めるとコンパイラーに指示しています。loop count プラグマはループの反復回数が特に大きな場合、特に小さい場合、そして特定の反復回数が使われる頻度が高い場合に有効です。

void foo(float *restrict a, float *b, int n, int num1){
   int i;
#pragma loop count avg(100000)
   for (i=0; i<n; i++)
      a[i] += b[i];
   }
}

③   浮動小数点演算の精度について

浮動小数点演算において「同じ精度」であることは、「ビットパターンのレベルで同じ演算結果」とは必ずしも同義ではありません。また、演算の順序を変えることによって「同じ結果」でなくなることもありますので注意が必要です。256 ビットの AVX コードに移行する際に初めて表面化する場合も少なからず見受けられるようですので、ここで取り上げておきます。

まずは算術関数について。sin() などの算術関数は、スカラーコードで使われる libm の関数とベクトルコードで使われる SVML(短ベクトル長算術ライブラリー)の関数があり、デフォルトの設定では SVML の関数のほうが若干精度が低い代わりに高速に演算が行われます。

XMM の SVML 関数(たとえば SSE4.2 向けのコード生成によるもの)と YMM の SVML 関数(AVX向けのコード生成によるもの)では同程度の精度ですが、同じ入力値でも戻り値が必ずしもビットパターン・レベルでは一致しない場合がありえますので注意が必要です。算術演算の演算精度は以下のオプションで制御することが可能です。

Linux / Mac OS X Windows
-fimf-absolute-error=value[:funclist] /Qimf-absolute-error:value[:funclist]
-fimf-accuracy-bits=bits[:funclist] /Qimf-accuracy-bits:bits[:funclist]
-fimf-arch-consistency=value[:funclist] /Qimf-arch-consistency:value[:funclist]
-fimf-max-error=ulps[:funclist] /Qimf-max-error:ulps[:funclist]
-fimf-precision=value[:funclist] /Qimf-precision:value[:funclist]
-fimf-domain-exclusion=classlist[:funclist] /Qimf-domain-exclusion:classlist[:funclist]

次に演算の順序が変わることで演算結果が変わりうることについて説明します。以下のような縮約加算のループは、ベクトル演算とスカラー演算で結果が異なる場合があります。指数部の値が大きく違う浮動小数点数を扱う場合には「丸め誤差」の扱いに注意してください。XMM から YMM への移行の際にも同様な注意が必要となります。/fp:precise (Windows*) または –fp-model precise (Linux*/MacOS*) によってこのようなループのベクトル化を抑制することができます。

float sum = 0.0f;
for(i=0;i<n;i++){
   sum += A[i];
}

スカラー演算の場合(A[0]+A[1] の結果に A[2] を加算する部分和)

A[0]
+ A[1]
+ A[2]

XMM のベクトル演算の場合(A[0]+A[4] の結果に A[8] を加算する部分和)

{A[3], A[2], A[1], A[0]}
+ {A[7], A[6], A[5], A[4]}
+{ A[11], A[10], A[9], A[8]}

ループの終了後に 4 要素の部分和から総和を計算。

YMM のベクトル演算の場合(A[0]+A[8] の結果に A[16] を加算する部分和)

{A[7], A[6], A[5], A[4],A[3], A[2], A[1], A[0]}
+ {A[15], A[14], A[13], A[12],A[11], A[10], A[9], A[8]}
+{ A[23], A[22], A[21], A[20] A[19], A[18], A[17], A[16]}

ループの終了後に 8 要素の部分和から総和を計算。

また、#pragma simd の vectorlength 節によってベクトル長に制約を加える(例えば 4 要素並列のベクトル化に限定)ことも可能です。

float sum = 0.0f;
#pragma simd vectorlength(4)
for(i=0;i<n;i++){
   sum += A[i];
}

④   AVX 命令と SSE 命令との間での状態遷移ペナルティーを避ける

256 ビットの AVX 命令を使用すると、YMM レジスターの上位 128 ビットが使用されたというプロセッサー・ステートに遷移します。この状態で SSE4.2 以下の SSE 系命令を実行すると、YMM レジスターの上位 128 ビットを不変にするため(本連載第 1 回の addps の例を参照)のペナルティーが発生します(不変にしたというステートに遷移)。さらに、この状態で 256 ビットの AVX 命令を実行すると内部ステートをもとに戻すためのペナルティーが発生します。以下のいずれかの方法を用いることにより、ペナルティーを避けることが可能です。

  • vzeroupper 命令を使用して YMM レジスターの上位 128 ビットをすべてゼロにしたというステートに遷移させる
  • SSE4.2 以下の SSE 系命令を使用せずに、128 ビットの AVX 命令を使用する
  • インテル® コンパイラーの AVX ターゲット・オプションで再コンパイルすることで上記 2 項目の最適化をほぼ自動化することが可能

⑤   16 バイトのロードは 2 つ同時実行可能

「整数データを扱っているので第 2 世代インテル® Core™ プロセッサーへの最適化はあまり関係ない」と思われている方も一部におられるかも知れませんが、256 ビット対応でロード命令の実行ユニットが強化されているので、既存の実行コードがそのままで早くなっていたり、AVX のターゲットオプションで再コンパイルすることで早くなったりすることもあります。

第3回のまとめ

今回は AVX へ最適化する際に留意すべき点の中から5 つの項目に触れました。最初の 3 項目は特に AVX 向けというわけではありませんので、既存の SSE4.2 向けのコードにも適用可能です。前回も今回もボトムアップ的な視点で見てきましたが、最終回となる次回はトップダウン的な視点でまとめます。

著者紹介

齋藤 秀樹氏。インテル コーポレーション ソフトウェア開発製品部門 シニア・スタッフ・エンジニア。2000 年の入社以来インテル® コンパイラーの性能評価および並列化の実装に従事するとともに、SPEC High Performance Group にて SPEC OMP、SPEC HPC2002、SPEC MPI2007 などのベンチマーク開発にも携わる。2007 年よりベクトル化の開発主任を担当し現在に至る。並列計算システムの分野で長年の経験と実績。多くの米国特許、論文および国際学会発表等の共著を持つ。

関連記事