DirectXの話 第166回

DXRのパフォーマンスの話

19/09/13 up

今年のCEDECでは DirectX Raytracing をやってみた系の話がいくつかあったのですが、コナミさんの講演では最適化部分にも触れられていて、ちょっと触発されたので実際にパフォーマンス計測を行ってみました。

サンプルとしては以前に作成した Sample016 を使用。

このサンプルは Multi Draw Indirect のサンプルではありますが、DXRを使って Ambient Occlusion の計算を行っています。

このレイトレ部分をいじって、何がどのように影響を与えるのかチェックしてみた、というのが今回の話です。

なお、NVIDIA さんのこちらのブログを参考にしてテスト内容を決定しています。

Tips and Tricks : Ray Tracing Best Practices

前提条件

今回使用したのは Sample016 で、どのような条件かを以下に箇条書しておきます。

    • 解像度は 1920x1080

    • メッシュは Crytek Sponza

    • レイトレースはデノイザありのAOとシャドウのみ

      • Closest Hit Shaderなし

      • Any Hit ShaderはOpaque以外のマテリアルに対してのみあり

      • AOのレイの数は1フレームあたり4本

      • レイの距離は短め

Bottom Layer Acceleration Structureの構築時フラグ

Bottom Layer Acceleration Structure (BLAS) の構築時に設定するフラグによって構築時間や後のレイトレース時間に影響を与えます。

D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BUILD_FLAGS はAS構築時に指定するフラグで、いくつかの組み合わせが考えられます。

特に速度に影響しそうなのは以下でしょう。

FAST_TRACE と FAST_BUILD は排他ですが、ALLOW_UPDATE と ALLOW_COMPACTION は同時に使用することができます。

これらのフラグの組み合わせがAS構築、及びレイトレースにどのような影響を与えるのかを調べた結果は以下です。

見て分かる通り、FAST_TRACE と FAST_BUILD の差は一目瞭然です。

トレース時間を考慮するなら FAST_TRACE を使うべきですし、静的オブジェクトについては FAST_TRACE で良いでしょう。

更新が頻繁に行われるものについては複雑さも考慮して FAST_TRACE と FAST_BUILD を使い分けるといいかと思いますが、AS構築を非同期コンピュートで処理するなら FAST_BUILD は選ばなくてもいいかもしれませんね。

ALLOW_COMPACTION はどの場合でもAS構築に余分な時間がかかります。

しかし、レイトレース時間には大きなインパクトを与えていません。

絶対とは言えませんが、静的オブジェクトはメモリ量削減のために、基本は ALLOW_COMPACTION を入れておいたほうがいいかもしれません。

面白いのは ALLOW_UPDATE で、通常このフラグは構築時間に影響を与えるはずです。

確かに FAST_BUILD の場合は構築時間が少々伸びているのですが、FAST_TRACE の場合はむしろ大きく下がっています。

何度か計測してもこの結果が出ているので、誤差というわけではないと思いますが、不思議ですね。

D3D12_RAYTRACING_GEOMETRY_FLAGSフラグ

AS構築時にジオメトリ情報に設定するフラグとして D3D12_RAYTRACING_GEOMETRY_FLAGS が存在します。

このフラグで重要なのは D3D12_RAYTRACING_GEOMETRY_FLAG_OPAQUE です。

このフラグを立てられてジオメトリは、不透明メッシュとしてマークされ Any Hit Shader が動作しなくなります。

Any Hit Shader へのルートが構築されないので、フラグを立てておくと高速化に繋がります。

もちろん、Any Hit Shader を使う場合はフラグを立ててはいけません。

この計測では単純にフラグをONにするだけでなく、Any Hit Shader を含めた Hit Group を使用するかどうかという条件も加えています。

計測結果は以下です。

・Any Hit Shader なしの Hit Group のみを使用

・Any Hit Shader ありの Hit Group のみを使用

・Maskedマテリアルのみ Any Hit Shader ありの Hit Group を使用

Any Hit Shader を使用していないとしても OPAQUE フラグの有無によって差が出ています。

OPAQUE フラグを立てた場合は Any Hit Shader がスキップされるので、Hit Group に Any Hit Shader があっても問題ないはずです。

計測結果には若干の差が出ていますが、このくらいなら誤差範囲かと思います。

計測時間にはやはり変動があり、変動を目視でチェックしている感じだとほとんど差がないように見えました。

最後の実験では不透明マテリアルとMaskedマテリアルを正しく分けて計測した結果です。

こちらはわずかではありますが安定した差が出ていました。

やはり Any Hit Shader を使わない場合は、正しく OPAQUE フラグを立てておくべきでしょう。

Any Hit Shader の処理の中身

Hit Group に Any Hit Shader を含めない場合と、Any Hit Shader は含めるけどそのシェーダでは何もしない、という状況ではどのような結果が出るでしょうか?

何もしない Any Hit Shader は最適化されて関数自体が消えてしまっているのかもしれませんが、特に何もしなければ Any Hit Shader なしの Hit Group を設定したことと同じになるようです。

比較すると、テクスチャサンプリングして…というきちんとした Masked 処理はなかなかに高価な処理と言わざるを得ません。

Masked や半透明マテリアルはラスタライザでも処理が重くなる筆頭ではあるのですが、レイトレーシングでも同様か、それ以上にインパクトがあると考えるべきでしょう。

TraceRay時のフラグ

TraceRay時にフラグを使うことでパフォーマンスに影響を与えることがあります。

RAY_FLAG_CULL_BACK_FACING_TRIANGLES, RAY_FLAG_CULL_FRONT_FACING_TRIANGLES フラグはレイトレースでヒットするポリゴンの裏表をチェックするフラグです。

いわゆる陰面消去を行うためのフラグですが、ラスタライザの場合は適切にカリングすることで無駄な処理を減らすことができます。

しかし、レイトレースについては常にそうとは言えず、レイトレース時の裏表チェックが重くなる場合があります。

RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH フラグは最初にレイがヒットした際にそこで処理をやめてしまうフラグです。

Any Hit Shader が有効な場合に IgnoreHit() を呼ばれた場合は更にレイの交差判定が行われますが、そうでない場合は即終了します。

シャドウやAOのように遮蔽されているかどうかだけが必要な処理の場合は、どのポリゴン・マテリアルに衝突したかは関係なく、どれに対してでも遮蔽されているかどうかが重要になります。

そのような場合にこのフラグを使うことで、交差判定を減らすことができるというフラグです。

これら3つのフラグの組み合わせ (CULL_BACK と CULL_FRONT は排他) によってどのようにパフォーマンスに影響があるか調べた結果は以下です。

Sponza のメッシュでバックフェイスカリングを行うと、シャドウやAOのレイでMaskedマテリアルの草類がヒットしなくなります。

これは調べる位置から光源方向にレイを飛ばすためで、草類は太陽の方向に向いてる面が Front 扱いだからです。

処理としては NONE と CULL_FRONT はほぼ同じレイトレース結果を返しますが、差は歴然ですね。

逆に NONE と CULL_BACK はMaskedマテリアルありとなしで結果が逆転しています。

Maskedマテリアルありの場合はバックフェイスカリングのおかげで Any Hit Shader が動作せず、その分軽くなっていると考えられます。

重い Any Hit Shader が動作しない場合はカリングしない方が有利です。

Maskedを使用しないシーンをレイトレースするのであれば、カリングは行わないほうが無難です。

ただ、カリングによって無駄な処理が省ける可能性があるなら検討の余地はあるでしょう。

END_SEARCH フラグはどのような場面でも高速化に寄与しています。

もちろん、使用する場面によっては使えないフラグなのですが、遮蔽のみをチェックする場合は必ず使用しましょう。

ペイロードサイズ

シェーダ内で情報をやり取りするためのペイロードデータはできるだけ小さくしたほうが良い、と先のブログには書かれています。

ここではペイロードサイズを変化した場合のパフォーマンスをチェックしてみました。

ただ、シェーダ内では大きなペイロードは不要なので、ペイロードの構造体としてはサイズ変更していますが、実際に書き込みを行うのは Miss Shader で float x 1 分のみです。

なぜかペイロードサイズを増やした方が高速化できています。

ペイロードサイズが 4~32 Bytes の場合はほとんど差が出てないのですが、64 Bytes の場合は確実に差が出るようになっていました。

で、ちょっと気になったので、シェーダ内で定義されたペイロードは 4 Bytes にして、パイプラインステートの Shader Config に設定するペイロードサイズは 64 Bytes にしてみました。

なお、上の結果はシェーダ内のペイロードサイズと Shader Config のペイロードサイズは同一にしています。

結果としてはペイロードサイズが 64 Bytes のときと同じ結果になりました。

僅かではありますが、なぜかパフォーマンスが良くなっているというわけです。

何度か計測を行っても同じ結果となっているので、環境依存かドライバの問題かはわかりませんが、Shader Config に設定するペイロードサイズは大きめにしておいたほうがいいのかもしれません。

追記:

NVIDIA Nsightを使用して少し細かくプロファイルを行ってみました。

結果としてですが、ペイロードサイズ4BytesのときはSM Occupancyが高く、64BytesのときはSM Occupancyが低くなっていました。

SM OccupancyはSM占有率で、基本的に高いほうが効率よくコアが動いている状態と言えます。

これだけ見るとペイロードサイズは小さい方が有利です。

64Bytesの際にSM Occupancyが低くなる要因はレジスタ不足によるもののようで、ペイロードサイズが大きくなるとその分レジスタを多く使うことになるようです。

ではなぜ高速化したのかというと、L2 Hit Rateが2%ほど64Bytesの方が高くなっていました。

L2 Hit RateはL2キャッシュのヒット率で、これが高いというのはシェーダリソースへのアクセスが効率よく動いているということになります。

今回のサンプルはさほど複雑な計算をしておらず、処理の重さの一番の要因はAny Hit Shaderによるシェーダリソースへのアクセスです。

ボトルネックがシェーダリソースアクセスなので、ここが改善することで高速化したと考えられます。

多分ですが、SM Occupancyが低下することでアクセス頻度が低下し、キャッシュ効率がむしろ改善したという状態のようです。

正確に何が要因かは不明ですが、@koguchitさんのツッコミがとても的確なので、このあたりが要因だと思われます。

@koguchitさんの該当ツイート

@koguchitさん、ありがとうございました!

Global Root Signature vs Local Root Signature

シェーダリソースは Global Root Signature と Local Root Signature のどちらに入れておくべきか?

これについては公式的には Global Root Signature にできるだけ配置したほうがいいということらしいです。

しかし、CEDECの発表を見ると、NVIDIAの中の人が特にどちらでも速度に影響はないと語っていたとか。

ならば試してみよう!

といっても、すべて Global RS、すべて Local RSというのはちょっと面倒だったので、Sample016そのままの状態と、Global RS の中で全フレームで固定可能なリソースは Local RS に入れるような形で計測してみました。

中途半端な計測環境ではありますが、特に Local RS に移したほうが遅くなる、ということはありませんでした。

リソース管理等を考慮して自分にとって面倒ではない方法でリソースを扱えばいいのではないかと思います。

最後に

今回の計測はAOとシャドウという、遮蔽だけ求められればOKというDXRの使い方をしているため、最適化手法がその他の手法 (リフレクションやGI) でも同様に使えるかは不明です。

また、今後ハードウェア、ソフトウェアの変更によって結果が変わる可能性も出てきます。

本格的にDXRを使用する場合はきちんと自分でパフォーマンス計測を行いましょう。

また、今回は大雑把に時間変化だけチェックしていますが、より複雑なシーン・条件になってくるとボトルネックもきちんと調べる必要が出てくるはずです。

そのような場合は NVIDIA さんの Nsight を使って細かく調査するほうが良いかと思います。