インテル® コンパイラーによる AVX 最適化入門: 第1回 AVX とは

インテル® DPC++/C++ コンパイラーインテル® Fortran コンパイラー

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

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

第1回 AVX とは

第1回の今回はソフトウェアの側から見た AVX について説明します。

AVX の主な特徴は以下のように大きく分けることができます。

SIMD 演算のオペランドサイズについて

先日、Haswell 新命令とも呼ばれる AVX2 拡張命令セットが発表になりました(https://software.intel.com/en-us/avx/)ので、そちらを含めた表を以下に示します(チック・トック・モデルではSandybridge、Ivybridge、Haswell の順になります)。XMM レジスターは 128 ビット、YMM レジスターは 256 ビットで、YMM レジスターの下位 128 ビットが同じレジスター番号の XMM レジスターになっています。

SSE4.2 AVX AVX2
整数演算 XMM XMM XMM および YMM
浮動小数点演算 XMM XMM および YMM XMM および YMM

上表で見るように、AVX では浮動小数点 SIMD 演算の 256 ビットへの拡張、AVX2 では整数 SIMD 演算の 256 ビットへの拡張が行われました。メモリーオペランドを使用する命令についても同様に 16 バイトの XMMWORD から 32 バイトの YMMWORD に拡張になっています。

この AVX 拡張により、従来は 4 要素並列 (4 x 32bit = 128bit) までであった単精度浮動小数点演算が 8 要素並列 (8 x 32bit = 256bit) に、2 要素並列 (2 x 64bit = 128bit) までであった倍精度浮動小数点演算が 4 要素並列 (4 x 64bit = 256bit) となり、並列度の倍加による浮動小数点演算性能の向上が見込まれます。今後実装される AVX2 を含め、SIMD 並列度を向上させることが高性能化への鍵になると考えてよいでしょう。

3 オペランド命令書式の採用と命令コードの2進表記の改良

AVX においては、従来の SSE4.2 までで採用されていた 2 オペランド書式から 3 オペランド書式に改良されました。単精度浮動小数点の加算命令を例に挙げると、命令の動作は次表のとおりになります。この改良により、入力レジスターと異なる出力レジスターを指定可能となり、レジスター間のコピーをする命令を減らす効果が期待されます。

指定できるオペランドの数を単純に 2 から 3 に増やすとその分だけ 1 命令に使うバイト数が多くなるわけですが、命令コードの 2 進表記の改良により実行コードサイズの増加を防ぐことができ、また場合によってはサイズが減少することもあります。高級言語のソースレベルで最適化できることではありませんが、AVX コードの性能を理解するうえで、SSE4.2 以下の SSE 系命令と AVX 命令とを区別する必要が後にでてきますので、説明に含めておきました。

Windows での ASM 表記 動作
SSE4.2 addps  xmm0, xmm1 xmm0 と xmm1 を SIMD 加算し xmm0 に代入。 ymm をサポートするプロセッサーにおいては ymm0 の上位 128 ビットは不変。
AVX 128 ビット vaddps  xmm0, xmm1, xmm2 xmm1 と xmm2 を SIMD 加算し xmm0 に代入。 ymm0 の上位 128 ビットはゼロを代入。
AVX 256 ビット vaddps  ymm0, ymm1, ymm2 ymm1 と ymm2 を SIMD 加算し ymm0 に代入。

マスク付きロード命令およびストア命令の追加

vmaskmov はマスク付きのロードまたはストアを行う命令です。以下のような条件分岐のあるプログラムにおいて、コンパイラーは b[i] へのアクセスを安全に行うことができるかどうか(すなわち、b[i] が常に参照可能なメモリを指しているか)判断不可能なため、従来は自動ベクトル化を断念するか、その部分だけスカラーのコードで(非効率的に)実行していました。

マスクつきロード命令により、b[i] へのメモリーアクセスを a[i]>0 の条件が真の時にだけベクトル実行することが可能になり、自動ベクトル化においても効率よく実行することができるようになりました。

void foo(float *restrict a, float *b, int n) {
   int i;
   for(i=0; i<n; i++){
       if (a[i]>0) {
         a[i] += b[i];
       }
   }
}

SIMD 演算命令のメモリーオペランドのアドレス境界要件の撤廃

従来の SSE4.2 までは SIMD 演算命令(例えば addps)がメモリーオペランドを使用する場合、その実行時論理アドレスは 16 バイトの XMMWORD 境界に沿っていなければならない、すなわちアドレスの下位 4 ビットがゼロである必要がありました。AVX の SIMD 演算命令ではこの制約を撤廃し、実行時のアラインメント・エラーが起きないように改良されました。

AVX を使う

以上、ソフトウェアの観点から見た AVX について簡単にご紹介させていただいたわけですが、それではどのような方法を使えば AVX を使えるのでしょうか?

インテル® マス・カーネル・ライブラリー (インテル® MKL) やインテル® インテグレーテッド・パフォーマンス・プリミティブ(インテル®IPP) などのすでにプロセッサーごとに最適化されているライブラリーを使用

既存のプログラムが MKL や IPP などのプロセッサーごとに最適化されたライブラリーを使用している場合、ライブラリー内部で実行中のプロセッサーを判断して最適なコードを選択し実行しますので、既存のプログラムに手を加えることなく AVX を利用することが可能です。古いバージョンのライブラリーをお使いの場合には最新のバージョンにアップグレードしてリンクし直す必要があるかもしれません。

第2世代インテル® Core™ i7 プロセッサーをターゲットにプログラムを再コンパイル

  • インテル® コンパイラーのターゲットオプション /QxAVX(Windows* の場合)、-xAVX(Linux*/Mac OS*の場合)を使用します。
  • インテル® コンパイラーはサードパーティーのオプション /arch:AVX(Windows* の場合)や -mavx(Linux*/Mac OS* の場合)も受け付けます。
ターゲットオプションを指定して再コンパイルすることにより、(コンパイラーの能力の範囲内で)AVX に最適化したコードを生成します。既存のプログラムに対して、SSE4.2 以下のターゲットへのコンパイラーによる自動ベクトル化がうまく適用できているようであれば、AVX への自動ベクトル化の成功確率は十分に高いと考えてよいでしょう。

AVX の C++ ベクトルクラスを使用

インテル® コンパイラーのパッケージの中に dvec.h というヘッダーファイルがあり、その中に F32vec8 および F64vec4 というクラスが定義されています。単精度の浮動小数点を 8 要素、もしくは倍精度の浮動小数点を4要素扱うためのクラスです。

次項で述べる AVX の SIMD 組み込み関数を C++ のルック&フィールで扱うためのクラス定義だと考えていただければよいと思います。蛇足ですが、128 ビット向けには F32vec4 および F64vec2 のクラスが以前から用意されています。

AVX の SIMD 組み込み関数を使用

SSE4.2 までの SSE 系命令を利用するために SIMD 組み込み関数を使用していた方はご存知のとおり、AVX にも SIMD 組み込み関数が用意されています。SSE 系の SIMD 組み込み関数のプログラムをインテル® コンパイラーで AVX ターゲットに向けて再コンパイルしますと 128 ビットの AVX コードを生成します。残念ながら自動での 256 ビット AVX 化は行われません。

(インライン)アセンブリー言語で書く

最終手段といっても過言ではないと思いますが、アセンブリー言語で AVX のプログラムを書くことも可能です。

AVX の例

最後に、簡単な AVX のコードを見てみましょう。

void foo(float *restrict a, float *b, int n){
   int i;
   for (i=0; i<n; i++){
      a[i] += b[i];
   }
}

上記の関数を二通りにコンパイルしてアセンブリー・コードを生成してみました。一番目は SSE2 のコード。a[] へのストアが 16 バイト境界になるようにした後、b[] のアドレス境界によって 2 つあるバージョンのどちらかを選んで実行します。アセンブリー・コードのループを 1 回まわるごとに 8 要素の加算が行われています。

icc –restrict –O2 –S
..B1.12:     ;;; 8 要素を扱うループ(バージョン 1)
      movups    (%rsi,%rax,4), %xmm0
      movups    16(%rsi,%rax,4), %xmm1
      addps     (%rdi,%rax,4), %xmm0
      addps     16(%rdi,%rax,4), %xmm1
      movaps    %xmm0, (%rdi,%rax,4)
      movaps    %xmm1, 16(%rdi,%rax,4)
      addq      $8, %rax
      cmpq      %rdx, %rax
      jb        ..B1.12
      jmp       ..B1.17
..B1.15:     ;;; 8 要素を扱うループ(バージョン 2)
      movaps    (%rdi,%rax,4), %xmm0
      movaps    16(%rdi,%rax,4), %xmm1
      addps     (%rsi,%rax,4), %xmm0
      addps     16(%rsi,%rax,4), %xmm1
      movaps    %xmm0, (%rdi,%rax,4)
      movaps    %xmm1, 16(%rdi,%rax,4)
      addq      $8, %rax
      cmpq      %rdx, %rax
      jb        ..B1.15
..B1.17:

二番目は AVX のコード。a[] へのストアが 32 バイト境界になるようにした後に以下のループを実行します。アセンブリー・コードのループを 1 回まわるごとに 16 要素の加算が行われています。

icc –restrict –O2 –xAVX –S
..B1.11:     ;;; 16 要素を扱うループ
      vmovups   (%rsi,%r8,4), %xmm0
      vmovups   32(%rsi,%r8,4), %xmm3
      vinsertf128 $1, 16(%rsi,%r8,4), %ymm0, %ymm1
      vinsertf128 $1, 48(%rsi,%r8,4), %ymm3, %ymm4
      vaddps    (%rdi,%r8,4), %ymm1, %ymm2
      vaddps    32(%rdi,%r8,4), %ymm4, %ymm5
      vmovups   %ymm2, (%rdi,%r8,4)
      vmovups   %ymm5, 32(%rdi,%r8,4)
      addq      $16, %r8
      cmpq      %rdx, %r8
      jb        ..B1.11

なぜこのようなコードが生成されたのかは次回ご説明いたします。

第1回のまとめ

今回はソフトの側から見た AVX の特徴と AVX を使い始めるための方法についてご紹介しました。次回から 2 回にわたって AVX への最適化について見ていきます。

著者紹介

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

タイトルとURLをコピーしました