こんにちは、大阪Cygamesシニアゲームエンジニアの岩﨑です。
CEDEC2020では『マルチプラットフォーム環境で実現するLLVM ClangによるSIMD自動ベクトル最適化』を講演させていただきました。
今回はオンライン開催となり初めてのことも多かったですがYouTubeでライブ配信もされたため多くの方に聴講頂きました。聴講頂き御礼申し上げます。
本講演ではゲーム開発環境でエンジニアが直面する性能最適化について、新しいアプローチが提案出来ればという思いで事例を紹介致しました。Clangの自動ベクトル化(Auto Vectorization)を効果的に機能させるためにコンパイラーにヒントを与えて誘導するテクニックの紹介とその結果についての話になります。
以下が講演資料です。
LLVM / Clang がもたらす高度な最適化
LLVMはコンパイルやリンクでプログラムを最適化するように設計されたコンパイラー基盤です。
この上で様々なプログラミング言語が構文解析として機能し、LLVMはその実行を最適化する部分を担います。言語仕様の変更は構文解析のレイヤーで変更することができ、あらゆる状況下でもベストなコード生成を担うのがLLVMです。Clangもその一つでLLVM上で動作するC/C++/Objective-C/ Objective-C++コンパイラーになります。
その最適化は記述の前後だけでなく、広範囲に渡って意味解析されロジックの組み換えまで行った高度な最適化が適用されます。ロジックの組み換えは「Loop Vectorizer」「SLP Vectorizer」で展開され、各アーキテクチャに適応した命令が出力されるようになります。
本講演ではこの2つの特徴を把握し、最適化性能を最大限に引き出す記法で記述することで有効活用しよう、という内容となります。
実用したときの所感
実際に使ってみた結果について、配列をループする処理については非常に強力で、SIMD化が積極的に行われておりパフォーマンスも手動で記述した場合と同じ性能が出ました。
この点に関しては非常に良い結果で、今回の事例紹介に至るモチベーションはこの一点に尽きるといっても過言ではありませんでした。しかしながら問題点もいくつかあり、いつでも簡単に使えるとまではいかない点もあります。長所に関してはスライド内で詳細を掲載していますので、ここでは短所を取り上げてみたいと思います。
実装が複雑すぎる場合SIMD最適化をコンパイラーが諦めてしまった瞬間に全てがバラバラになる
ループ内が全てスカラー演算になってしまうため、急に性能が大幅に低下してしまいます。
そのため、一旦最適化記述が終わったコードに追記するときは慎重に結果を見ながら行う必要がありました。
アライメントをコンパイラーにヒントとして与えることが重要
アライメントが合っていない可能性がある、とコンパイラーが判断した場合はその前提のコードが出力されるため最高性能が出なくなります。注意を払う必要がありました。
基本的には配列構造でデータを保持する必要がある
SLPVectorizerをフル稼働させるためには配列の状態で連続したデータが存在している必要があります。リスト構造のように点在してしまうとパイプラインが阻害されるため最適化が働かなくなります。キャッシュヒット率も性能に左右されるためデータ志向設計が重要になってきます。
※詳しくは当ブログ「データ志向設計」をご参照ください。
やはりSIMDの仕様を全く知らない状態で記述することは難しい
コンパイラーによって最終的にはSIMD命令に変換されます。そのときにどのような命令に変換されるかある程度予想しながら記述すると良好な結果に誘導できます。
中途半端な実装になると前述のコンパイラーがSIMD化を諦めることにもつながりかねません。
CPUに優しいフローを心掛ける必要があります。
基本的には2の乗数の個数で配置、分岐命令を極力排除、三項演算子の活用です。
実際に運用する場合に懸念されること
自動ベクトル化は最適化のアシスト機能の一つであり必ずしも良い結果を生むわけではありません。そのため手放しで喜べるものではありませんが、最適化の敷居が下がるというメリットは大きいです。特に複数の環境でアーキテクチャ自体に差異がある場合はこのような言語レベルの抽象的なレイヤーでの最適化は便利です。
前提として基本的な最適化の理論やCPUやキャッシュの挙動については知っておく必要があり、コンパイラーの最適化を誘導することで効果的に自動最適化を活用できるようになります。
※ただ単に書けば速くなるわけではないということはご留意ください。
又、コンパイラーの最適化オプションを無効化したときには、最適化あり/なしで顕著な性能差が発生します(数百倍もの差になります)。デバッグ開発中に性能の低下が問題になる場合は特定モジュールのみ最適化を常に有効にするなど配慮も必要になってきます。
ループ数やアライメントの都合上、最大限に自動ベクトル化の性能を引き出して記述できる箇所はそこまで多くないと考えられます。個数に端数がある場合は切り上げてSIMD要素単位でループするほうが性能が出ますので、設計の段階でデーター構造を考慮しておくことも重要です。
配慮しなければならない事項が増えるため、コスト面で優れるかどうかは場合によりそうです。又、処理する粒度によっては従来の組み込み関数による手動最適化と使い分けることが好ましいです。
今回の内容は特に可読性について優位性があると考えられます。ニーモニックを熟知していないエンジニアでも処理内容を把握しデバッグトレースできることは一つの長所です。環境が変わっても良好なコードを出力できるかは保証されているものではありませんので都度結果の確認だけは必要になってくると思います。
手動記述では見られない異なるニーモニックでスケジューリングが行われたりする点も興味深いです。局所最適化は組み込み関数、それよりも一回り大きいループを含む実装は自動ベクトル化にする等適材適所で運用するとより良いと思います。
理想は全てがこれで上手く行けば素晴らしいですが、経験上万能ではないことは明らかです。使いどころを見極めて適切に使っていくことが重要です。
最後に
実行速度最適化はゲームの品質を高める要素の一つであると同時にエンジニアの手腕が問われる部分です。最終的にはその道で経験を積んだエンジニアがプロファイリングと改善を地道に繰り返すというストイックな努力によって実現されることが良い結果につながる、という揺るぎない真理があります。
クリエイター全体で意識を高めて問題に取り組むことが出来れば「処理負荷が重いのでこの表現や機能は削除しなければならない」という事案を一つでも多く減らすことが出来るようになります。それは実際に手に取ってくれるユーザーの満足に繋がっていくと信じています。
アーティストは表現を吟味し、エンジニアはそれを一つの作品に詰め込む時にこのような努力を日々行っています。
その時の方法の一つとして今回の事例が役に立てばと思います。