マルチスレッドに対応する際、参考にしたのがマイクロソフトのサンプルだけど、このサンプルって実は特殊なのでは無いかと思った。
複数のコマンドリストを用意して、それぞれを担当するスレッドがコマンドリストにコマンドを追加していく。
これはある1シーンのためにカスタマイズされた専用のプログラムで使う分には問題ないし効果も高そう。
ただ、自分が作っている汎用的な3Dエンジンの場合、ちょっと無理があるかもしれないと思い始めた。
今までは1つのコマンドリストにシーケンシャルにコマンドを追加していた。
必要なパラメータを順番に追加していけば描画はされた。
新しくマルチスレッド対応になった場合、シングルスレッドで追加するか、マルチスレッドで追加するかを切り替えられるようにした。シングルスレッドの場合は今までと同じ使用感。
マルチスレッドにした場合、最初のコマンドリストだけで実行するコマンド、全部のコマンドリストで同じ実行が必要なコマンド、パラレルに設定できるコマンドなど呼び側でかなりいろいろ考慮が必要で、投入コマンドがかなり多くないと寧ろ遅くなる結果になっていた。
DX12 Do's And Don'ts
nVIDIAのサイトでこんなドキュメントを見つけた。
全てきちんと理解できたらかなりクオリティが高くなりそうだけど、読み取れた一部でも改良していきたい。
ExecuteCommandListsとFence
PIXで見るとExecuteCommandLists単位で、隙間が開いている。
これが普通なのかと思っていたけど、全くダメな作りだった。
PSOの切り替え
シェーダが同じものをまとめて描画し、ExecuteCommandListsを呼び出していた。
それしかできないと思っていた。
勘違いしていたのが、Reset関数にPSOを指定するからなんだけど、Resetのタイミングで指定するのだから切り替えにはまたResetする必要があるのだろうと。だが実は違った。
SetPipelineStateという関数があり途中から設定できる。設定できるだけでなく切り替えも出来る。
これはnVIDIAのドキュメントに書いてあるわけではないけど、読んでいるうちにそういう事が出来るのだろうと、リファレンスを調べたらあった。
SetPipelineState、Set[Graphics|Compute]RootSignatureを呼べば途中でシェーダの切り替えも出来ることを知った。
コマンドリストは15~30以下と書かれていて驚いた。
これをみて今までの考え方が根本から間違っているのではないかと思った。
シェーダを切り替える度にコマンドリストも切り替えて行くといいのではないか?
同時に複数のコマンドリストを扱うのではなく、常に1つのコマンドリストに対して追加し、切り替えのタイミングで次のコマンドリストに移る。ある程度コマンドリストが溜まったらExecuteCommandListsを実行する。
ドキュメントではすべてのコマンドリストを用意してから最後にExecuteCommandListsというのもだめパターンに書かれている。5回~10回に分けて、CPUとGPUが並行処理していく状態を作るのが望ましい。
マルチスレッド対応した際、コマンドリストに投入したいデータが出揃ったタイミングで、一斉にスレッドに追加させていた。
これもマイクロソフトのマルチスレッドのサンプルを最初に見てしまった弊害。
追加する度に別スレッドで並列にコマンドリストにも追加するように作り変えた。
SetPipelineState、Set[Graphics|Compute]RootSignatureを呼べば途中でシェーダの切り替えも出来ることを知った。
コマンドリストは15~30
コマンドリストは15~30以下と書かれていて驚いた。
これをみて今までの考え方が根本から間違っているのではないかと思った。
シェーダを切り替える度にコマンドリストも切り替えて行くといいのではないか?
同時に複数のコマンドリストを扱うのではなく、常に1つのコマンドリストに対して追加し、切り替えのタイミングで次のコマンドリストに移る。ある程度コマンドリストが溜まったらExecuteCommandListsを実行する。
ドキュメントではすべてのコマンドリストを用意してから最後にExecuteCommandListsというのもだめパターンに書かれている。5回~10回に分けて、CPUとGPUが並行処理していく状態を作るのが望ましい。
マルチスレッド対応した際、コマンドリストに投入したいデータが出揃ったタイミングで、一斉にスレッドに追加させていた。
これもマイクロソフトのマルチスレッドのサンプルを最初に見てしまった弊害。
追加する度に別スレッドで並列にコマンドリストにも追加するように作り変えた。
全部のコマンドを作っていざ実行となったときに、今まではこのタイミングから追加し始めていたのが、裏で作るのと同時にコマンドリストに追加をしているため、待ち時間が短縮できた。
描画全体の時間(デバッグ)
揃ってからスレッド実行 :0.0958秒
追加と同時にスレッド実行:0.0707秒
描画全体の時間(リリース)
揃ってからスレッド実行 :0.0642秒
追加と同時にスレッド実行:0.0398秒
ただ残念なことに、マルチスレッドにせずメインスレッドで直接コマンドリストに追加した場合は下記となり、マルチスレッドが意味ないと思わせる結果となった。
デバッグ:0.0618
リリース:0.0289
コマンドリストに追加する重たい処理が少ないケースだからか?
改良版はそこまでシングルスレッドに負けてないので、マルチスレッドが勝つケースもあると信じる。
比較
シェーダを切り替える度にExecuteCommandListsを呼び出していたのを、まとめて1回で実行したものの比較。
![]() |
| Z Pre-passからDeferredRenderingまでをまとめて実行 |
修正前は、塊の間隔が空いていて無駄に待ってしまっている。全体は343usかかっている。
修正後は、まとめた部分が並列で動くようになっており、全体は297usに短縮している。
PIXでイベント名をつける単位がExecuteCommandListsの実行単位なので、今まできれいに分類されていたのが、1まとめになってしまったけど、そんなことはどうでもいいくらいにパフォーマンスがアップした。
拡張バリアにした結果、400us位かかっていたのが、350usぐらいになって、コマンドリストの実行をまとめた結果、350usから300usぐらいになった。
後半のEmissive部分がまだまとめる余地が残っているのでもう少し削れそう。


0 件のコメント:
コメントを投稿