Upgrade to Pro — share decks privately, control downloads, hide ads and more …

マルチプラットフォーム環境で実現するLLVM ClangによるSIMD自動ベクトル最適化

510ec964f5d26c2724c883fd7b671e3d?s=47 Cygames
September 02, 2020

マルチプラットフォーム環境で実現するLLVM ClangによるSIMD自動ベクトル最適化

2020/09/02 CEDEC2020

510ec964f5d26c2724c883fd7b671e3d?s=128

Cygames

September 02, 2020
Tweet

Transcript

  1. 1/64

  2. 自己紹介 2015年より株式会社Cygamesに所属。 PC/ハイエンドコンシューマープラットフォームでの研究開発に従 事。グラフィクス分野を中心にゲームエンジン開発や技術支援・ ワークフロー改善などを担当。現在は大阪Cygamesでのタイトル を担当し、より高い品質のコンテンツ制作を進めている。 2/64 株式会社Cygames 技術本部 コンシューマー

    シニアゲームエンジニア 岩﨑 順一
  3. はじめに 近年のコンパイラーはコード最適化が成熟しつつあります。 とりわけLLVM Clangでは構文解析をClangが担い、最適化エンジ ンをLLVMが担うことで言語仕様の導入反映の早さと高水準の 実行バイナリ最適化を両立しています。 その中でも自動ベクトル最適化は標準的な言語仕様上の範疇で大き な性能向上を果たしています。 本セッションではこの自動最適化に着目して恩恵を最大化するため 効率的に最適化が適用されるコード記法を紹介します。

    3/64
  4. 内容 Clangの自動ベクトル化について – LLVMのOptimizerの強力なベクトル化 インラインアセンブラや組み込み関数を使わないSIMD化 – C++の言語標準の記法でSIMD命令を利用 コンパイラーにSIMDを認識させる記法について – 認識に必要な条件を満たすため注意すべき点

    実装例&実際のコンパイル結果の比較 4/64
  5. LLVM Clangの自動ベクトル化 - about Automatic Vectorization - 5/64

  6. LLVMについて • Low Level Virtual Machine (低水準仮想マシン) @Wikipedia • コンパイル時、リンク時、実行時などあらゆるフェーズで

    プログラムを最適化するよう設計された低レベルVM • 言語仕様を解析するフロントエンドを実装し、中間コードを出力。 • LLVMは最適なコードバイナリに変換する。 Clang Rust Swift LLVM x86 x64 ARM PPC 構文解析と中間コード出力のみ(軽量) 最適なコードバイナリを生成 (最適化エンジン) Shader binary DX12 HLSL 6/64
  7. AutoVectorization (自動ベクトル化)とは 単一のスカラー処理を自動でSIMDレジスタに割り当てて 並列化。ループ展開でパイプライン最適化を行う機能 – SIMD演算リソースを有効に活用する流れ – 10年以上前から最適化が進み、実用レベルになりつつある – 言語標準の範疇で自動で適用。

    – Clang, RustなどはLLVMによる強力な自動ベクトル化が適用される 2000年初頭にはAutoVectorizationに特化したC++コンパイラ 「VectorC」なども登場 https://vectorc.apponic.com/ 7/64
  8. LLVM の 2種類の自動ベクトル化 ループベクタライザ (Loop Vectorizer) ループ内の命令を自動展開して複数の連続した反復処理に拡張 – ループを展開で分岐命令が除去されたコードに変更 (loop

    unroll) – ループ展開は命令キャッシュやパイプライン実行のトレードオフ SLPベクタライザ (Superword Level Parallelism Vectorizer) コード内で見つかった複数のスカラーをベクトルに結合 – 条件が揃った場合に通常のループ処理を対応するSIMD命令に置換 – Clangでは特に強力で大幅な性能向上。 8/64
  9. パイプライン実行 各ユニットを『流れ作業』にして効率化するCPUの仕組み – ライン生産方式のようにバケツリレー。各回路は単純になり効率向上 全体としては待ち時間が隠蔽される(スループットの向上☺) 結果が得られるまでの遅延がある(レイテンシの増大) IF MEM ID EX

    WB IF MEM ID EX WB IF MEM ID EX WB IF MEM ID EX WB IF MEM ID EX WB IF MEM ID EX WB IF MEM ID EX WB IF MEM ID EX WB IF MEM ID EX WB IF MEM ID EX WB IF ID EX MEM WB (Instruction Fetch) (Instruction Decode) (EXecution/address calculation) (Memory Access) (Write Back) 9/64
  10. パイプライン実行 • パイプライン実行が阻害される要因 – 実行順序を変更する分岐処理 • if/else, while, do/while, switch/case,

    continue, break, goto など • 関数コールや関数からのreturn – 先読みする動作が切り替わってしまう – CPUは命令読込の段階からやり直しが発生 (パイプラインフラッシュ) • 軽減策として『分岐予測ユニット』がやり直しを削減 – 静的な分岐(goto/jmp命令など)は分岐先が固定されている – 真偽判定を規則性や傾向から確率高いほうを選ぶ etc.. 10/64
  11. 一般的なパイプライン実行の効率化対策 小さい関数はインライン展開でcall/return両方の負荷削減。 func(); func(); func(); func(); void func() { process();

    } func(); func(); func(); func(); inline void func() { process(); } コンパイラ最適化によって inline指定なし関数も自動展開することもある process func() process func() process func() process func() process process process process 11/64
  12. 一般的なパイプライン実行の効率化対策 • ループ処理は小さいループを展開して複数個でループ。 • ループ判定処理が占める割合も同時に低減可能 for( int i=0; i<16; ++i

    ) { process(i); } for( int i=0; i<16; i+=4 ) { process(i+0); process(i+1); process(i+2); process(i+3); } if process process process process if process if process if process if process 4要素一括程度が効率的な場合が多い。 (演算レイテンシの隠蔽効果) ※多すぎると逆にキャッシュ効率低下するため注意 12/64
  13. ループベクタライザ (Loop Vectorizer) 関数のループ展開を自動展開するベクトル最適化 – 最適化時に既存コードも条件に合致すれば暗黙的に展開 – LLVM Loop Vectorizerでは複雑なループをベクトル化可能。

    – LLVMの世代バージョンで最適化効率も進化。 意図的に利用したい場合 コンパイラーのベクタライザに検知してもらいやすい記法を知る必要がある。 13/64
  14. ループベクタライザが検知する記法 値のfor, while, do-while, C++11範囲ベースfor ループ – switch-case文は複雑な処理と見做され展開されない • 将来的にサポートされる可能性はある?

    偶数回数・2の乗数回(2,4,8,16,…)のほうが展開効率が良い – ベクトル化のループ展開幅に一致しない場合は 前半に展開された処理が実行される。 – 奇数個など端数の部分は後半に単純コードで実行される。 追加処理用のコードサイズが増大。 14/64
  15. ループ展開例 (1) ループ個数が不明な場合の展開サポート void foo(float *A, float* B, float K,

    int start, int end) { for (int i = start; i < end; ++i) A[i] *= B[i] + K; } 未知のループカウントを持つループをサポート。 o コンパイル時点ではstartとendの位置は不明でゼロから始まる保証がない o 個数がベクトル幅の倍数ではない可能性 ループ展開された処理と端数の処理に自動分割される 15/64
  16. ループ展開例 (2) void foo(float* A, float* B, float K, int

    n) { for (int i = 0; i < n; ++i) A[i] *= B[i] + K; } ポインタエイリアスによる実行時チェック 高速動作にはrestrict キーワードでポインタが重複メモリを指さない明示が必要。 ※コンパイラ差異が吸収できている拡張__restrict__で利用することもおすすめ AとBが重複アドレスを指す可能性がある。 Aの書き込みによってBの内容が変化(自己破壊)するため 厳密に処理する場合は値の再ロードが発生 void bar(float * __restrict__ A, float* __restrict__ B, float K, int n) 16/64
  17. ループ展開例 (3) int foo(int *A, int *B, int n) {

    unsigned int sum = 0; for (int i = 0; i < n; ++i) sum += A[i] + 5; return sum; } 数値の集計合算 ループ内で合計を計算する場合など 1つの変数に依存関係がある場合にも対応。 加算、乗算、XOR、AND、ORなど様々な演算をサポート。 合計値を集計する変数がローカル変数 17/64
  18. ループ展開例 (4) ループカウンタの並列化 void foo(float *A, float* B, float K,

    int n) { for (int i = 0; i < n; ++i) { A[i] = i; } } ループカウンタは変化する値だが検知される。 ループ値も含めたループ展開最適化が有効。 ループカウンタを値として利用している場合 18/64
  19. ループ展開例 (5) ループ内のif変換 int foo(int *A, int *B, int n)

    { unsigned int sum = 0; for (int i = 0; i < n; ++i) { if (A[i] > B[i]) sum += A[i] + 5; } return sum; } 一般的に冗長とされるループ内ifにも対応。 内容によって分岐無し (branchless) 最適化の対象になる。 ループ内にif文がある場合 19/64
  20. ループ展開例 (6) ポインターやイテレーターを用いたループ処理の展開 int foo() { return std::accumulate(v.begin(), v.end(), 0);

    } Clangではポインタのループカウンタを検知。 一般的なC++プログラムはイテレーターを使用するためこの展開は非常に強力。 ※この例のstd::accumulateは内部ループ内にC++イテレータが使用されている 20/64
  21. ループ展開例 (7) 逆イテレータ int foo(int *A, int *B, int n)

    { for (int i = n; i > 0; --i) A[i] +=1; } 逆方向のループに対しても検知して展開。 先述のポインタ最適化のように逆イテレータも利用可。 21/64
  22. ループ展開例 (8) メモリのScatter / Gatherの展開 int foo(int * A, int

    * B, int n) { for (intptr_t i = 0; i < n; ++i) A[i] += B[i * 4]; } メモリをstride飛び地アクセスする場合を検知。 一般的には非効率とされる計算だが、 Intel Architectureでは2の乗数でのstrideメモリアクセスに対応。高効率に展開。 2の乗数の倍数2,4,6,8,16…でループ 22/64
  23. ループ展開例 (9) 混合型のベクトル化 int foo(int *A, char *B, int n,

    int k) { for (int i = 0; i < n; ++i) A[i] += 4 * B[i]; } 型が異なる値同士の演算の展開に対応。 LLVMでは型変換のコストを計算推定され効果的と判断される場合は 自動でループを展開。 23/64
  24. ループ展開例 (10) 広範囲のエイリアス分析でのループ展開 struct { int A[100], K, B[100]; }

    Foo; int foo() { for (int i = 0; i < 100; ++i) Foo.A[i] = Foo.B[i] + 100; } 静的な変数などメモリ配置が重複していない、と検知できる場合は strict構文不要でループ展開検知。 24/64
  25. ループベクタライザ まとめ 関数のループ展開を自動展開することができる。 – 最適化時に既存コードも条件に合致すれば暗黙的に展開。 コンパイラをClangに変更するだけで高速化。 – ClangではLLVMの恩恵で複雑なループをベクトル化可能 ベクタライザに検知してもらいやすい記法記述すること。 –

    シンプルなforループ、偶数個、2の乗数の値をループに利用。 意外な処理がループ展開されて高速化される場合あり。 – LLVMのOptimizerが非常に優秀。 25/64
  26. SIMD自動ベクトル化 - SIMD Automatic Vectorization - 26/64

  27. SIMD並列化について 27/全ページ SIMD (Single Instruction Multiple Data) – 日本では “シムド”、英語では

    “シムディー”と発音される。@wikipedia – 演算装置での1回の命令で複数データに対する処理を同時に行う SISD (Single Instruction Single Data) o 1命令で単一のデータを扱うノイマン型アーキテクチャ @wikipedia
  28. Intel CPUアーキテクチャと命令セット Intel AVX-512 (Advanced Vector Extensions 512) o 512bit長のZMMレジスタ。レジスタ数も16本から32本に倍増。

    o Skylakeから一部搭載。 o Floatの場合は16要素 Intel SSE/2/3/4/4.1/4.2 (Streaming SIMD Extensions) – 128bit XMMレジスタ。Floatの場合は4要素 Intel AVX/AVX2 (Advanced Vector Extensions) o 256bit長のYMMレジスタ。3/4オペランド非破壊命令も利用可。 o Floatの場合は8要素 28/64
  29. これまでのSIMD最適化実装 intrinsic組み込み関数での記述 – C++コード最適化との親和性の問題でインラインアセンブラは廃止 – 専用の組み込み関数を利用することでSIMD命令に変換される。 #include <mmintrin.h> MMX #include

    <xmmintrin.h> SSE #include <emmintrin.h> SSE2 #include <pmmintrin.h> SSE3 #include <tmmintrin.h> SSSE3 #include <smmintrin.h> SSE4.1 #include <nmmintrin.h> SSE4.2 #include <ammintrin.h> SSE4A #include <wmmintrin.h> AES #include <immintrin.h> AVX, AVX2, FMA ※Intel Architectureの場合 inline XMVECTOR XMVectorZero() { #if defined(_XM_NO_INTRINSICS_) XMVECTORF32 vResult = { { { 0.0f, 0.0f, 0.0f, 0.0f } } }; return vResult.v; #elif defined(_XM_ARM_NEON_INTRINSICS_) return vdupq_n_f32(0); #elif defined(_XM_SSE_INTRINSICS_) return _mm_setzero_ps(); #endif } ※組込み関数の記述例 DirectXMath より 29/64
  30. これまでのSIMD最適化実装 プラットフォーム固有の実装を書かなければならない – ソースコードのCPU依存コードが増える – 新規のCPU命令セットには個別対応が必要。 – #ifdef や実装分割があることで可読性低下 アセンブラ命令を熟知する必要がある

    – 部分的な最適化はなんとかなるが… – コード全体に波及させることは物量的に困難 – 実装スキルに依存するため人を選ぶ 保守 労力 コスト 30/64
  31. SLPベクタライザ(Superword Level Parallelism Vectorizer) • 通常のスカラー実装でSIMD化可能と判断されるコードを • 自動でSIMDレジスタに割り当てをするベクタライザ float a[4];

    float b[4]; float c[4]; for( int i=0; i<4; ++i ) { c[i] = a[i] + b[i]; } vmovaps xmm1, [a] vaddps xmm1, [b] vmovaps [c], xmm1 a b c SLPベクタライザ 31/64
  32. 自動SIMD化によるメリット 特殊な組み込み関数を実装する必要が無くなる – マルチプラットフォーム移植性の向上 – 最適化にLLVMの恩恵を最大限に受けることが可能。 • 対象のCPUアーキテクチャで実装可能なSIMDに自動変換 • 最適化が優秀。ほぼ理想的なパイプラインが出力される。

    • LLVMのバージョンアップでOptimizerが進化する。 一般的なC++構文で記述。可読性がアセンブラよりも良い。 – 比較的処理内容を追いやすく、デバッグする場合にも効果的 32/64
  33. 自動ベクトル化のための記法 - Notation for automatic vectorization - 33/64

  34. SIMD化のための記述 ループ同様、コンパイラに検知してもらいやすい記述が必要。 – ループ展開よりも判定がシビア – 満たすべき条件がループ展開よりも多い – 偶然ベクトル化される可能性はそれほど高くない。万能ではない。 検知されなかった場合はスカラー演算になってしまう。 –

    記述通りの従来通りの挙動になる – 逆アセンブル表示で意図するSIMD命令になっているかどうか 目視で確認しながらの利用をおすすめ。 34/64
  35. 自動ベクトル化の条件 変数配列アドレスが命令セットのアライメントであること ポインタエイリアスで最適化阻害されてないこと – 関数の引数でポインタ渡しで最適化する場合 restrict で対応が必要。 ループ回数がSIMDレジスタ要素数の倍数であること コンパイラ最適化オプションに有効化設定が必要 35/64

  36. 変数のアライメント 命令セットによって必要なアライメントが異なる – SSE2 – AVX/AVX2 – AVX-512 Intel Architectureはアライメント合致しない場合でも動作

    – アライメントが揃っている場合 vmovaps が生成 – アライメントが揃っていない場合 vmovups が生成 アライメント不一致はキャッシュライン性能低下の要因 ※アライメントが一致している場合はvmovupsでも性能低下は発生しない アライメント境界 36/64 = 16byte = 32byte = 64byte
  37. 変数のアライメント C++11以降ではalignasが利用可能 – 言語標準で移植性も問題なし alignas(16) float a[64]; struct alignas(16) A

    { }; 今後AVX512など将来的にメインストリームになるアーキテクチャでの 良好なコード出力にも期待することを考慮する場合64以上のメモリ境界を指定 MSVC __declspec(align(32)) int array[8]; 【参考】従来の各環境のアライメント指定の記法の差異 Clang / gcc int array[8] _attribute((aligned(32)); 37/64
  38. 変数のアライメント ポインター渡しの場合はアライメント情報が欠落する問題 – コンパイラーは変数のアライメント情報をポインタに渡さない – 関数の呼び先でSIMD化が阻害されてしまう まさか、全部テンプレートで実体参照でインライン展開するしかない!? alignas(16) float a[64];

    func(a); void func(float *a) { } a.cpp b.cpp ポインターは呼び出し元の 16byteアライメントを認識しない! 38/64
  39. C++20 std::assume_aligned コンパイラーに対してメモリのアライメントを伝える void func (int* p) { int* p1

    = std::assume_aligned<64>(p); } 上記 p1は64バイト境界としてコンパイラーが認識。 o p1を使った自動ベクトル化ではvmovapsが正しく生成される! o 但し、pが実際に64バイト境界を指しているかどうかは別。 • あくまでもコード生成のヒントになる。 • 安全のためassertで実アドレス境界をチェックすることを推奨 39/64
  40. std::assume_aligned 互換実装例 template<std::size_t N, typename T> [[nodiscard]] constexpr T* assume_aligned(T*

    ptr) { #if defined(__clang__) || (defined(__GNUC__) && !defined(__ICC)) // Clang / gcc の場合 return reinterpret_cast<T*>(__builtin_assume_aligned(ptr, N)); #elif defined(_MSC_VER) // MSVC の場合 if ((reinterpret_cast<std::uintptr_t>(ptr) & -static_cast<std::intptr_t>(N)) == 0) return ptr; else assume(0); #elif defined(__ICC) // Intel C++ compiler の場合 switch (N) { case 2: __assume_aligned(ptr, 2); break; case 4: __assume_aligned(ptr, 4); break; case 8: __assume_aligned(ptr, 8); break; case 16: __assume_aligned(ptr, 16); break; case 32: __assume_aligned(ptr, 32); break; case 64: __assume_aligned(ptr, 64); break; case 128: __assume_aligned(ptr, 128); break; } return ptr; #else return ptr; // 未知のコンパイラ #endif } Clang / gccの場合はbuiltin命令が対応 MSVCは2020年9月の時点では 拡張命令なし。今後対応される可能性。 アライメント判定のみ入れた状態。 IntelC++は個別で分岐する記法。 C++20 導入されるまでの互換実装 コンパイラーの独自言語拡張で記述することで 先行した対応が可能 40/64
  41. ループ回数をSIMD要素数の倍数に 必要要素数の倍数であればSIMD命令が出力される – 2倍の値を設定すると2グループ単位で展開可能 • レジスタ消費によって実行効率が下がる場合もある • 要ベンチマーク。ベンチマーク結果が正義。 命令セットや型によって必要な要素数が異なる o

    SSE2 = char x16 / short x8 / float x4 o AVX/AVX2 = char x32 / short x16 / float x8 o AVX-512 = char x64 / short x32 / float x16 AVX-512やAVX-1024を想定した要素数にしておくと将来にも対応できる 41/64
  42. 自動ベクトル化のためのオプション設定 Clangの場合 (Intel) – nativeの箇所にCPUアーキテクチャ名を固定で記述することも可能 – 分散ビルド環境下ではデフォルトの -march=native では各PC環境の アーキテクチャでコンパイルされてリンク時に混在してしまう。

    その場合はCPUアーキテクチャを固定したほうが良い。 MSVCの場合 ※Visual Studio 2019 version 16.3 以降ではAVX512にも対応 42/64 -Ofast -march=native -ffast-math 指定 または 、 指定 /O2 /Ox /fp:fast /arch:アーキテクチャ
  43. AutoVectorizationでのSIMD記法 基本記法 – アライメント指定された配列を確保 – 型とSIMD要素数に合ったループ数 4,8,16,32,64…の倍数で演算 alignas(32) float x[32];

    alignas(32) float y[32]; alignas(32) float result[32]; for( int i=0; i<32; ++i ) { result[i] = x[i] * 2.0f + y[i]; } 変数アライメントを指定。 ポインターの場合は std::assume_aligned<N>で指定 ループ数をSIMD要素数の倍数に このループが SIMD化対象 43/64
  44. AutoVectorizationでのSIMD記法 一時変数はローカル変数で明示的に定義 – ローカル変数とスコープで一時変数レジスタを明示できる。(生存寿命が短く局所的) – メモリアクセスを無くす最適化として機能 void func(const float* __restrict__

    x, float* __restrict__ result) { x = std::assume_aligned<32>(x); result = std::assume_aligned<32>(result); for( int i=0; i<4; ++i ) { for( int n=0; n<32; ++n ) { result[i] += x[n] + n; } } } void func(const float* __restrict__ x, float* __restrict__ result) { x = std::assume_aligned<32>(x); result = std::assume_aligned<32>(result); alignas(32) float temp[32] {}; for( int i=0; i<4; ++i ) { for( int n=0; n<32; ++n ) { temp[i] += x[n] + n; } } for( int n=0; n<32; ++n ) { result[i] = temp[n]; } } 引数の領域を一時変数に して集計計算をしている ローカル変数上で 集計計算。 このスコープのみで明示 最後にローカル変数を結果に出力 ✔ スタックに退避されたりなど 意図しない最適化結果になる場合がある 44/64
  45. AutoVectorizationの最適化対象 SSE/AVXが持つ四則演算/論理演算 – 通常のC言語の記述で変換される – A * B + C

    の記法で積和命令を生成 (CPUアーキテクチャが対応している場合) ※それ以外の記法の場合は認識しないことが多い 一部のstdのmathライブラリ関数 pow exp exp2 sin cos sqrt log log2 log10 fabs floor ceil fma trunc nearbyint など 条件式 – If文での数値比較 – 同一配列index同士の比較はSIMD化されるためさらに高速。 (マスクプレディケーション) 45/64
  46. AutoVectorizationの最適化対象 条件式が最適化された事例 – if文で分岐無しの0xffffffffと0のマスクビット生成 alignas(16) float x[32]; alignas(16) float y[32];

    alignas(16) int mask[32]; for( int n=0; n<32; ++n ) { mask[n] = (x[n] > y[n]) ? 0xfffffffful : 0; } 一見ループ内にある非効率なif分岐処理に見えるが、 1命令のSIMD比較命令vcmpltpsに変換され、比較でマスク値が生成される。 46/64
  47. LLVMによる強力な最適化 IEEE754 float 絶対値 std::fabsf の変換事例 – 整数0x7fffffffロード、float値に論理演算ANDで最上位bitマスクされる。 – この手法は高速化トリックの一つだがLLVMではSIMD自動生成。

    floatはバイナリでは最上位ビットが正負フラグになっている 要素のshuffle / broadcast o 配列代入で要素を入れ替えることでSIMD要素の交換命令生成 o 1要素を他の全要素に演算することでbroadcast命令を生成 o 前後の冗長な入れ替え処理は自動で最小の交換に自動最適化 31 30 23 22 0 sign exponent fraction 符号 指数部 仮数部 47/64
  48. 最適化チェックポイント • vmovupsを出力してしまっていないか? – アライメント指定の確認 • 一時変数をスタックに出力していないか? – ループ内一時変数をローカル変数にすることで良好な出力へ。 –

    ローカル変数にもアライメント指定を忘れずに。 • 意図しないスカラー命令になっていないか? – SSE命令が~psならOK。~ssならSIMD化失敗でスカラー命令。 – スカラー命令の場合はSIMD化できない条件を踏んでいる。 実際に逆アセンブル結果で検証を推奨 48/64
  49. 実例と比較 - Examples and comparisons - 49/64

  50. SIMD最適化のための一般的な前提事項 分岐命令は可能な限り削減 – パイプラインフラッシュと再充填までのストールを回避 – if分岐で単純代入値が変化する程度であれば三項演算子で記述 • 分岐なしの選択命令が出力される ループ展開についてはLoop Vectorizerが展開数を自動評価して適切に展開してくれる

    float value = (cond) ? a : b; 同一演算ユニットで完結するように設計 – 単一の float/int がスカラ命令になるとベクトルレジスタと レジスタ間を横断する必要があるため最適化阻害される。 Intelアーキテクチャでは ベクトル版_ps と、スカラ版 _ss が存在するためこの問題を回避可能 50/64
  51. SIMD最適化のための一般的な前提事項 AoS構造(Array of Structure)よりもSoA構造(Structure of Array) – 全てのSIMD要素が無駄なく利用可能。キャッシュ効率も良好 ☺ –

    並列度を高く設計できる 【例】データー志向設計 Data Oriented Design 予めSoA構造で設計しておくことで自動ベクトル化の検出精度も最大化される X Y Z - X Y Z - X Y Z - X Y Z - X Y Z - X Y Z - X Y Z - X Y Z - X Y Z - X X X X X X X X X X X X Y Y Y Y Y Y Y Y Y Y Y Y Z Z Z Z Z Z Z Z Z Z Z Z AoS構造(Array of Structure) SoA構造(Structure of Array) ✔ 51/64
  52. Clang vs MSVC – シンプルなループ比較 - void func() { alignas(64)

    f32 X[COUNT]; alignas(64) f32 Y[COUNT]; alignas(64) f32 Z[COUNT]; alignas(64) f32 R[COUNT]; for (int i=0; i<1024; ++i ) { R[i] = X[i] * 2.0f + Y[i] * Z[i]; } } アライメントが揃った配列で簡単な演算を行う比較検証 積和命令を期待して A * B + C の形式で記述 52/64
  53. Clang vs MSVC – シンプルなループ比較 - loop: vmovaps ymm0,ymmword ptr

    frustum[rax*4] vmovaps ymm1,ymmword ptr [rbx+rax*4+1260h] vaddps ymm0,ymm0,ymm0 vaddps ymm1,ymm1,ymm1 vmovaps ymm2,ymmword ptr view[rax*4] vmovaps ymm3,ymmword ptr [rbx+rax*4+2260h] vfmadd132ps ymm2,ymm0,ymmword ptr [rbx+rax*4+240h] vfmadd132ps ymm3,ymm1,ymmword ptr [rbx+rax*4+260h] vmovaps ymmword ptr proj[rax*4],ymm2 vmovaps ymmword ptr [rbx+rax*4+3260h],ymm3 add rax,10h cmp rax,400h jne loop loop: vmovups ymm1,ymmword ptr [rax+rcx+7800h] vmulps ymm2,ymm1,ymmword ptr [rax+rcx+6800h] vfmadd231ps ymm2,ymm3,ymmword ptr [rax+rcx+5800h] vmovups ymm1,ymmword ptr [rax+rcx+6820h] vmovups ymmword ptr [rax+rcx+8800h],ymm2 vmulps ymm2,ymm1,ymmword ptr [rax+rcx+7820h] vfmadd231ps ymm2,ymm3,ymmword ptr [rax+rcx+5820h] vmovups ymm1,ymmword ptr [rax+rcx+6840h] vmovups ymmword ptr [rax+rcx+8820h],ymm2 vmulps ymm2,ymm1,ymmword ptr [rax+rcx+7840h] vfmadd231ps ymm2,ymm3,ymmword ptr [rax+rcx+5840h] vmovups ymm1,ymmword ptr [rax+rcx+6860h] vmovups ymmword ptr [rax+rcx+8840h],ymm2 vmulps ymm2,ymm1,ymmword ptr [rax+rcx+7860h] vfmadd231ps ymm2,ymm3,ymmword ptr [rax+rcx+5860h] vmovups ymmword ptr [rax+rcx+8860h],ymm2 sub rax,0FFFFFFFFFFFFFF80h cmp rax,1000h jl loop Clang11 MSVC16.4.2 最新アーキテクチャでベクトル化可能☺ メモリアライメントが反映されている☺ vmovaps 積和演算命令のvfmaddも出力されている! Clang同様に自動ベクトル化可能☺ 但しメモリアライメントが反映されていない vmovups 53/64
  54. Clang vs MSVC – 複雑なループ比較 - for (u32 plane =

    0; plane < Frustum::Plane::Max; ++plane) { alignas(32) f32 s[SIMD_SIZE]; alignas(32) u32 r[SIMD_SIZE]; for (u32 n = 0; n < SIMD_SIZE; ++n) { s[n] = (sphereX[i * SIMD_SIZE + n] * frustum.planes_[plane * 4 + 0] + frustum.planes_[plane * 4 + 3]) + (sphereY[i * SIMD_SIZE + n] * frustum.planes_[plane * 4 + 1]) + (sphereZ[i * SIMD_SIZE + n] * frustum.planes_[plane * 4 + 2]); r[n] &= (s[n] < sphereR[i * SIMD_SIZE + n]) ? 0xfffffffful : 0; } } この部分の結果を抜粋 視錘台と球の簡易交差判定の例 (厳密な判定ではない軽量判定) 浮動小数点演算による積和と比較演算、整数演算によるマスクが 同時にループ内に存在 54/64
  55. Clang vs MSVC – 複雑なループ比較 - Clang11 MSVC16.4.2 演算意図がSIMDで拾い上げられている☺ vmovss

    xmm4,dword ptr [r8+r11+9804h] vmovss xmm5,dword ptr [r8+r11+9808h] vmovss xmm6,dword ptr [r8+r11+980Ch] vmovss xmm7,dword ptr [r8+r11+9810h] vmovss xmm8,dword ptr [r8+r11+9814h] vmovss xmm9,dword ptr [r8+r11+9818h] vmovss xmm10,dword ptr [r8+r11+981Ch] lea edx,[r10*4] mov ecx,r14d vmovss xmm14,dword ptr [r11+rdx*4+0D800h] lea eax,[rdx+3] vmovss xmm13,dword ptr [r11+rax*4+0D800h] lea eax,[rdx+1] vmovss xmm12,dword ptr [r11+rax*4+0D800h] vmulss xmm1,xmm12,dword ptr [r8+r11+6800h] lea eax,[rdx+2] vmovss xmm11,dword ptr [r11+rax*4+0D800h] vmulss xmm0,xmm11,dword ptr [r8+r11+7800h] vaddss xmm3,xmm1,xmm0 vmulss xmm1,xmm14,dword ptr [r8+r11+5800h] vaddss xmm2,xmm1,xmm13 vmulss xmm1,xmm11,dword ptr [r8+r11+7804h] vaddss xmm0,xmm3,xmm2 vcomiss xmm0,dword ptr [r8+r11+9800h] vmulss xmm2,xmm12,dword ptr [r8+r11+6804h] vaddss xmm3,xmm2,xmm1 vxorps xmm0,xmm0,xmm0 vmovd eax,xmm15 cmovb ecx,eax mov eax,ecx スカラー演算になって命令数が画面に収まらない程に大幅に増加。 複雑になるとベクトル化を早い段階で諦めてしまう模様 vbroadcastss ymm5,dword ptr [rcx+rbx-0Ch] vbroadcastss ymm6,dword ptr [rcx+rbx] vbroadcastss ymm7,dword ptr [rcx+rbx-8] vbroadcastss ymm8,dword ptr [rcx+rbx-4] vfmadd132ps ymm5,ymm6,ymmword ptr [rsp+1C0h] vfmadd231ps ymm5,ymm7,ymmword ptr [rsp+180h] vfmadd231ps ymm5,ymm8,ymmword ptr [rsp+140h] vcmpltps ymm5,ymm5,ymmword ptr [rsp+100h] vandps ymm4,ymm5,ymm4 mov ecx,r14d vcvtsi2ss xmm0,xmm0,rax vmovss dword ptr [r8+r11+9800h],xmm0 vmulss xmm0,xmm14,dword ptr [r8+r11+5804h] vaddss xmm2,xmm0,xmm13 vaddss xmm1,xmm3,xmm2 vmulss xmm2,xmm12,dword ptr [r8+r11+6808h] vcomiss xmm4,xmm1 vmulss xmm1,xmm11,dword ptr [r8+r11+7808h] vaddss xmm3,xmm2,xmm1 vxorps xmm0,xmm0,xmm0 vpextrd eax,xmm15,1 cmova ecx,eax mov eax,ecx mov ecx,r14d vcvtsi2ss xmm0,xmm0,rax vmovss dword ptr [r8+r11+9804h],xmm0 vmulss xmm0,xmm14,dword ptr [r8+r11+5808h] vaddss xmm2,xmm0,xmm13 vaddss xmm1,xmm3,xmm2 vmulss xmm2,xmm12,dword ptr [r8+r11+680Ch] vcomiss xmm5,xmm1 vmulss xmm1,xmm11,dword ptr [r8+r11+780Ch] vaddss xmm3,xmm2,xmm1 vxorps xmm0,xmm0,xmm0 vpextrd eax,xmm15,2 : この例ではほぼ最善の最適化結果。 但し常時全てが意図する通りにベクトル化 されるとは限らない。 自動ベクトル化も万能ではないため過信は禁物。 55/64
  56. CPUアーキテクチャによる結果比較 下記の各命令セットの違いによる出力をClangのオプション設定を変えて検証 Intel (Naharem microarchitecture) SSE2 Intel (Haswell microarchitecture) AVX

    / AVX2 / FMA Intel (Skylake microarchitecture) AVX512 ARM (ARMv8 AArch64) A64 / SIMD and Floating-point Instruction 56/64
  57. Intel Naharem SSE2 movaps xmm4, xmmword ptr [rsp + 240]

    mulps xmm4, xmm3 addps xmm4, xmmword ptr [rsp + 192] movaps xmm7, xmmword ptr [rsp + 224] mulps xmm7, xmm2 movaps xmm5, xmmword ptr [rsp + 208] mulps xmm5, xmm0 addps xmm5, xmm7 addps xmm5, xmm4 cmpltps xmm5, xmm1 andps xmm5, xmm6 movaps xmm4, xmmword ptr [rsp + 176] mulps xmm4, xmm3 addps xmm4, xmmword ptr [rsp + 160] movaps xmm6, xmmword ptr [rsp + 144] mulps xmm6, xmm2 movaps xmm7, xmmword ptr [rsp + 128] mulps xmm7, xmm0 addps xmm7, xmm6 addps xmm7, xmm4 cmpltps xmm7, xmm1 movaps xmm6, xmmword ptr [rsp + 80] mulps xmm6, xmm2 movaps xmm4, xmmword ptr [rsp + 64] mulps xmm4, xmm0 x64の場合は標準搭載 128bit xmmレジスタ 8本 同時4要素のSIMD演算 積和演算を持たないアーキテクチャは mulとaddの組み合わせが出力された -msse2 57/64 2008年 11月
  58. Intel Haswell AVX / AVX2 / FMA vmovaps ymm11, ymm3

    vmovups ymm1, ymmword ptr [rsp + 736] vmovups ymm0, ymmword ptr [rsp + 704] vfmadd213ps ymm11, ymm1, ymm0 vmovups ymm4, ymmword ptr [rsp + 672] vfmadd231ps ymm11, ymm4, ymm14 vmovups ymm5, ymmword ptr [rsp + 640] vfmadd231ps ymm11, ymm5, ymm12 vcmpltps ymm11, ymm11, ymm13 vmovaps ymm8, ymm2 vfmadd213ps ymm8, ymm1, ymm0 vfmadd231ps ymm8, ymm4, ymm10 vfmadd231ps ymm8, ymm5, ymm9 vcmpltps ymm8, ymm8, ymm15 vmovaps ymm1, ymm3 vmovups ymm4, ymmword ptr [rsp + 608] vmovups ymm5, ymmword ptr [rsp + 576] vfmadd213ps ymm1, ymm4, ymm5 vmovups ymm0, ymmword ptr [rsp + 544] vfmadd231ps ymm1, ymm0, ymm14 vmovups ymm6, ymmword ptr [rsp + 512] vfmadd231ps ymm1, ymm6, ymm12 vcmpltps ymm1, ymm1, ymm13 vandps ymm1, ymm11, ymm1 vandps ymm7, ymm7, ymm1 256bit ymmレジスタ 16本 同時8要素のSIMD演算 レジスタ数増加でメモリアクセスが 低減されている FMA命令 (vfmadd積和命令)で レイテンシが削減されている -mfma –mavx2 58/64 2013年 6月
  59. Intel Skylake AVX512 vgatherdps zmm4 {k1}, zmmword ptr [rcx +

    4*zmm26] vfmadd213ps zmm24, zmm19, zmm18 vfmadd231ps zmm24, zmm17, dword ptr [r8 + rax + 16]{1to16} vfmadd231ps zmm24, zmm4, dword ptr [r9 + rax + 16]{1to16} vcmpltps k0, zmm24, dword ptr [r10 + rax + 16]{1to16} vinserti32x4 zmm6, zmm0, xmm6, 0 vpmovm2d zmm24, k0 vpsrld ymm8, ymm24, 31 kmovd k1, edi vpternlogd zmm24, zmm24, zmm24, 255 vpandq zmm24 {k1}, zmm25, zmm8 vbroadcastss zmm25, dword ptr [rdx + rax + 20] vfmadd213ps zmm25, zmm19, zmm18 vfmadd231ps zmm25, zmm17, dword ptr [r8 + rax + 20]{1to16} vfmadd231ps zmm25, zmm4, dword ptr [r9 + rax + 20]{1to16} vcmpltps k0, zmm25, dword ptr [r10 + rax + 20]{1to16} vpmovm2d zmm25, k0 vpsrld ymm8, ymm25, 31 vpternlogd zmm25, zmm25, zmm25, 255 vpandq zmm25 {k1}, zmm31, zmm8 vbroadcastss zmm31, dword ptr [rdx + rax + 24] vinserti32x4 zmm12, zmm0, xmm3, 0 vfmadd213ps zmm31, zmm19, zmm18 vfmadd231ps zmm31, zmm17, dword ptr [r8 + rax + 24]{1to16} vfmadd231ps zmm31, zmm4, dword ptr [r9 + rax + 24]{1to16} 512bit zmmレジスタ 32本 同時16要素のSIMD演算 レジスタ数がさらに倍増 メモリアクセスが低減されている 新命令でレイテンシが削減されている -mavx512f -mavx512dq -mavx512bw -mavx512vbmi -mavx512vbmi2 -mavx512vl 59/64 2015年 8月
  60. ARM - ARMv8 AArch64 A64 SIMD movi v10.2d, #0xffffffffffffffff stp

    x9, x9, [x10] add x11, x10, #4 ld1 { v10.s }[1], [x11] ldr q11, [x3, x8] ldr q2, [sp] ldr q9, [x4, x8] add x11, x10, #12 ld1 { v10.s }[3], [x11] fmla v12.4s, v2.4s, v11.4s fmla v13.4s, v6.4s, v11.4s fmla v14.4s, v18.4s, v11.4s fmla v15.4s, v22.4s, v11.4s fmla v24.4s, v26.4s, v11.4s fmla v28.4s, v31.4s, v11.4s fcmgt v11.4s, v9.4s, v12.4s fcmgt v12.4s, v9.4s, v13.4s fcmgt v13.4s, v9.4s, v14.4s fcmgt v14.4s, v9.4s, v15.4s fcmgt v24.4s, v9.4s, v24.4s fcmgt v28.4s, v9.4s, v28.4s and v9.16b, v11.16b, v10.16b and v9.16b, v9.16b, v12.16b and v9.16b, v9.16b, v13.16b and v9.16b, v9.16b, v14.16b A64 128bitレジスタ 32本 同時4要素のSIMD演算 A64 SIMDベクタ命令 強化されたNEON --target=aarch64-arm-none-eabi -mfloat-abi=hard 60/64
  61. まとめ - summary - 61/64

  62. まとめ 自動ベクトル化でスケールできる並列処理が記述可能 – 対応CPUアーキテクチャによって最適なSIMD命令を自動出力 – 記法を変えずに未知のCPUも含めた多くのCPUに対応できる。 SIMD命令の選択もコンパイラーに任せることができる – 手動で組み込み関数で書く場合とは異なる戦略で高速化 –

    LLVMのコストモデルで最適な命令が選択される – LLVMのバージョンが上がればさらに高速なコードへ アセンブラ命令を知っておくことは最適化の助けになる – クラッシュしたときのデバッグに。 – 適切な命令を出力するために記法でコンパイラーを誘導できる 62/64
  63. まとめ • 自動ベクトル化有効時と無効時で性能差が大きくなる – 特にSoA構造では差が顕著 – Vectorizationの冗長な記法もベクトル化OFFの性能差に影響 – DebugビルドとReleaseビルドの実行性能差も激しい。 最適化レベルを下げると自動ベクトル化が無効化される

    – Sanitizer有効時にも無効化される • 逆アセンブル結果を見て必ずチェック – SIMD化したと思っていたら!スカラー展開されていたら悲劇 – 非効率な命令になっていないかチェック! – より高効率な記法に調整していくときの指針に。 63/64
  64. 参考資料 Auto-Vectorization in LLVM https://llvm.org/docs/Vectorizers.html Auto-vectorization status in GCC, Clang,

    ICC and MSVC http://0x80.pl/notesen/2019-02-02-autovectorization-gcc-clang.html std::assume_aligned https://cpprefjp.github.io/reference/memory/assume_aligned.html SIMD Vector Classes for C++ (GitHub) https://github.com/VcDevel/Vc DirectXMath https://github.com/microsoft/DirectXMath “VC” SIMD Vector Classed for C++ (GitHub) https://github.com/VcDevel/Vc 64/64
  65. 65