拙訳
- Zen4世代のAMD Ryzen CPUの仕組みと最適化
Hardware Prefetcher
- 連続したデータを読むとき、CPUが次に使うデータを予測して、キャッシュに読み出しておいてくれる
- ストライドが固定であれば離れていてもOK
- 上り下りは問わない
Cache-Coherency Efficiency
- CPUは最大8コアでL3キャッシュを共有するCCXという単位で構成され、異なるCCX間のデータ転送はData Fabricを通るため、CCX内と比べて遅くなり得る
- キャッシュの一貫性を高めるためには以下のことに気をつける:
- 複数コアにまたがるキャッシュラインへの書き換えを各々で行うことは極力避ける
- アトミックな書き込みは各々で行わず、一旦ローカルで集計してから一括して行う
- スピンロックでは、一旦非アトミックにフラグを確認してからtest-and-setする
- ロックの効率を考えるなら、スピンロックでなく、モダンなAPIを使う
- mimallocやjemallocなどのマルチスレッド対応のメモリ管理ライブラリを利用する
AMD “Preferred Core”
- 一部のCPU製品には他のコアより性能が高いコアが存在するものがある
- スレッド・スケジューリングや電力管理に影響がでるため、Windows PCではスレッド・アフィニティを設定しないほうが良い
Best Practices
- CPUプロファイリングでは、リリースビルドを使う
- その際に有効化したデバッグ機能は、実際のリリース時に消し忘れないよう注意
- CPUプロファイリングでは、改ざん防止機能やアンチチート機能をオフにする
- リリースビルドで行うのが望ましいが、プロファイリングツールの正常動作を妨げるおそれがあるため
- 初回起動を再現するためには、シェーダのキャッシュを削除する
- AppData内のD3DSCache、AMD/DxCache、AMD/GLCache、AMD/VkCache
- Program Files/NVIDIA Corporation/NV_Cache
- Program Files (x86)/Steam/steamapps/shadercache
- コンパイラやWindows SDKは最新のものを使う
- ビルド時間の改善や実行時の最適化などの恩恵を受けられるため
- プロジェクトフォルダをアンチウイルスの保護対象から外す
- ビルド時間が改善するが、驚異から脆弱になるので、CI/CD環境では行わない
- 可能ならAVXやAVX2を必須環境にする
- Steamユーザーの9割がAVX対応CPUを使っている
- Windows10/11の必須環境はSSE2/SSE4.1からだが、Windows 10をサポートするAMD CPUならAVXがもれなく使える
- 開発ツールではAVX512を有効にする
- ライトのベイク、テクスチャの圧縮、メッシュのSDF生成、などで恩恵がある
- モダンな同期APIを使う
- 新しいAPIはベンダー固有の命令に変換されるので、システムコールを使わなくて済む
- std::mutexは、パフォーマンスも良好でCPU使用率も低いので、おすすめ
- WindowsだとSRWLOCK系が該当するが、std::mutexが内部で使っている
- WaitForSingleObject系は古いAPIなので重い
- スピンロックは速いが、CPUを使い尽くすのでバッテリー的に辛い
- 非アトミックにフラグを確認し、その後、アトミックにフラグを更新する
- ロックに失敗したら、
_mm_pause()を使ってCPUを休ませる
- memcpyでは、srcとdestのポインタをalignする
- alignas(64)だと、より高速なマイクロコードで実行される、かも
- alignas(4096)だと、store-to-loadコンフリクトを減らせる、かも
- clamp(bit_floor(バイト数), 4, 4096)にalignすると、キャッシュヒット率とアライメントのバランス的に良い
- コア数でスケールするようにコードを書く
- スレッド数は少なすぎず多すぎず、ハードコーディングしない
- PSOのコンパイルなどは論理コアすべてにスケールするのが理想
- 一方、ゲームプレイは、SMT効率の低下やキャッシュの競合などにより、物理コアだけのほうがパフォーマンス的に良くなるかも(AMDの場合)
- スレッド・アフィニティは使わない
- スケジューリングや電力管理に悪影響が出るため
- スレッドにはアフィニティではなく優先度を使う
- スレッドがストールした場合に備えて、低優先度のスレッドを泳がせておくほうが良い
- スレッド優先度のブーストに注意
- クリティカルセクションで待機すると、スレッド優先度が格上げされることがある
- 問題を起こしたら優先度ブーストを無効化してみると良い
- false sharingを回避する
- 異なるロックが同じキャッシュライン上にある、などすると発生する
- これを回避するにはalignas(64)にする
- 間接参照するデータにはソフトウェアのプリフェッチ命令を使う
_mm_prefetchを使って次に使うデータだけをフェッチしておくと、キャッシュラインの節約にもなる
- SSEとAVXを混ぜて使うさいのペナルティを回避する
- AVXとSSEの間で値を移行するとき、VZEROUPPERを使って上位128ビットをゼロ埋めしないとパフォーマンスが劇的に低下する
- 検知するには、PMCx00E Floating Point Dispatch Faults > 0を使う
__m256の出力を参照渡しで受けると、vzeroupper命令が挿入される- または、関数を
__forceinlineにする - そもそも、コンパイルフラグでAVXを有効化してしまえば、この問題は発生しない