【c/c++】AVX2を使ってみて少しだけ比較

結果の加算周りが非効率なためこの記事に載せたプログラムは遅いです。
この部分を修正した場合、AVX2無しで動かしたほうが高速です。
wrongwrong163377.hatenablog.com



記事中のAVX2命令を用いたプログラムはcmp命令を使っていないため低速な状態です。これを修正したものは以下。
wrongwrong163377.hatenablog.com


レポートが詰まった挙げ句AVX512命令に手を出し、Skylake-XとSkylakeを見間違えて「AVX512が動かないいい!*1」と発狂しながら時間を浪費していましたが僕は元気です。
AVX2を使ってみました。ついでにAVX2の有無&OpenMPによる並列化の有無で比較してみました。



基本的な使い方

本題に入る前にAVX命令の基本的な使い方をまとめます。

作成環境

Visual Studio 2017のCmakeプロジェクトで作成しました。

includeするもの

immintrin.hをincludeすればOKです。一応このヘッダを入れておけばSIMD命令全般が動かせます。
このヘッダの内容は参考文献を御覧ください。

Cmakeのオプション指定

OpenMPが混ざっててアレですが、AVX2を使うならフラグに/arch:AVX2を追加します。こちらも詳しくは参考文献を御覧ください。

cmake_minimum_required (VERSION 3.8)
find_package( OpenMP )
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${OpenMP_CXX_FLAGS} /O2 /arch:AVX2")
add_executable (CMakeProject1 "CMakeProject1.cpp" "CMakeProject1.h")
基本的な処理の流れ

AVX2(に限らずSIMD命令全般)を使った処理の流れは以下の通りです。

  1. 変数に対して、load命令で配列から読み込むか、set命令で1つの値でクリアして初期化。
  2. 演算を行う。
  3. store命令で配列にデータを書き出す。

要するに、演算前に処理しやすいようにまとめる処理と処理したものを配列の形に戻す処理が挟まるということですね。
add、mul、subのような基礎演算とset、load、storeのようなメモリへの読み書きが一番大きいでしょうか。この辺を使えるようになれば、探せばほかの操作もできると思います。
ただ、命名規則や実装内容が結構不規則な点は非常に使いにくいです。ほぼ自分も覚えられていません。

作ったもの

円周率をシミュレートするプログラムを以下のバリエーションで作成しました。

  1. シングルスレッド・AVX2無し(StNs)
  2. マルチスレッド・AVX2無し(MtNs)
  3. シングルスレッド・AVX2無し(StWs)
  4. マルチスレッド・AVX2有り(MtWs)
関数の処理の流れ

要するに原点を中心にした(1, 0)から(0, 1)までの半径1の1/4の円と(1, 0)から(0, 1)を対角線に持つ正方形を用意し、正方形の範囲に乱数的に点を沢山打つと、面積の比から円周率が計算できるというアレです。

  1. 乱数で0以上1以下の点(x, y)を生成。
  2. 座標の原点からの距離を計算。
  3. 結果が1以上なら円の外としてカウント*2
  4. (総試行回数 - カウント) * 4 / 総試行回数で円周率を取得*3
ソースコード

長いので折りたたみます。

出力例

num = 10000000での出力結果の例です*4

StNs    : 0.695000 sec. pi = 3.141440
MtNs    : 0.174000 sec. pi = 3.141440
StWs    : 0.605000 sec. pi = 3.141740
MtWs    : 0.166000 sec. pi = 3.141740

比較

実行環境
CPU Core i7 6700
モリー 16GB(2133C13)
OS Windows10
コンパイラ VC++2017*5
ビルドオプション /O2 /arch:AVX2 (x64 Release)
実行結果

numを変化させながら( X=\log_{10}num)として計測した結果です*6

X = 7 StNs MtNs StWs MtWs
実行時間 0.695 0.174 0.605 0.166
/StNs[%] 100 25.0 87.1 23.9
X = 8 StNs MtNs StWs MtWs
実行時間 6.221 1.681 5.836 1.648
/StNs[%] 100 27.0 93.8 26.5
X = 9 StNs MtNs StWs MtWs
実行時間 61.813 16.362 58.599 16.172
/StNs[%] 100 26.5 94.8 26.2
考察

X > 7ではAVX2を使っても全然早くなりませんでした。
というか乱数生成部が思いっきり足を引っ張っているようで、AVX2によるシングルスレッドの高速化という意味ではX > 7で頭打ちのようです。選んだ内容がアカンかった……。
やってみてから言うのもなんですが、こうして見ると「GPU使ったほうが速そうね」感が非常に強いです。ここまで最適化する意義が正直、その……。次遊ぶならOpenCLですかね?

参考文献

koturn様によるSIMD組み込み関数のまとめ。コンパイラ毎のコンパイルオプションや命令などが分かりやすくまとまっている。 SIMDの組み込み関数のことはじめ - koturnの日記
Intelによるリファレンス。 Intel Intrinsics Guide
@tanakmura様によるAVX512命令を用いたシャッフルプログラムのサンプル。 vshufps - Qiita
@yohholy様によるx86SIMD Intrinsicとヘッダファイルの対応のまとめ。 x86/SIMD Intrinsicとヘッダファイル対応表 - Qiita

*1:IntelのメインストリームにAVX512が降りてくるのはCannon Lake以降になりそうです。

*2:外側の方が面積が狭いので、カウント加算処理が少なくなるため(特にブロックが減るマルチスレッドで)高速化できます。

*3:4倍を先にやってしまうとオーバーフローするので最後に回します。

*4:乱数周りの実装がマズく、シングルスレッドの実行時間が1秒以内に収まると乱数の種が一致してしまうため、piの計算結果が次のマルチスレッドの実行結果と被ります。

*5:バージョンが確認できず。2018/6/29時点で最新のものだったはず。

*6:X > 9ではunsigned longの最大値も超えるので実行しない。