マテリアルのIfノードはHLSLでどう展開されるのか

前提:シェーダープログラムは分岐に弱いらしい

シェーダープログラムは GPU で実行されます。GPU は CPU と比較すると、if などの命令パイプラインに分岐が入る命令が苦手なのは有名な話です。 分岐が入ったプログラムでは、分岐の前後においてすべての実行コアが同じ命令を同じ時間実行していることが保証できなくなってしまい、SIMD のメリットを享受しにくくなってしまうためだと思います。

マテリアルの If ノードが気になる

UE のマテリアルノードはシェーダーコンパイルの過程でノード → usf (unreal shader file) → HLSL → ... と変換されていき、最終的に各プラットフォームで利用可能な形式になります。 ところで、UE のマテリアルには If ノードがあります。前述の通り、If はシェーダープログラムにおいてボトルネックになりやすいポイントらしいのですが、マテリアルノードで配置した If はシェーダーコード上ではどういった形式になるのでしょうか

サンプルマテリアル

今回は以下のようなマテリアルを対象として考えます。

sample material

極めて単純なマテリアルです。Base Color に If ノードを使った特に意味のない処理を接続しています。それ以外の値はデフォルト値のままです。

生成された HLSL コードを見てみる

マテリアルノードから生成された HLSL コードは、エディタの以下の位置から閲覧することができます。

show hlsl

code

ただ、このウィンドウはコードを読むには表示上も機能上も全く適していないので、Copy ボタンからコードをコピーし、お好きなエディタ上で閲覧することをおすすめします。

該当部の HLSL コード

hoge
void CalcPixelMaterialInputs(in out FMaterialPixelParameters Parameters, in out FPixelMaterialInputs PixelMaterialInputs)
{
    // 略
    MaterialFloat3 Local1 = ((abs(0.00000000 - 5.00000000) > 0.00001000) ? (0.00000000 >= 5.00000000 ? MaterialFloat3(1.00000000,0.00000000,0.00000000) : MaterialFloat3(0.00000000,0.00000000,1.00000000)) : MaterialFloat3(0.00000000,1.00000000,0.00000000));
    // 略
    PixelMaterialInputs.BaseColor = Local1;
    // 略
}

省略しまくっていますが、今回の If ノードに対応する箇所は上記のコードのように実現されていました。見ると、素朴な条件演算子によって実現されていることがわかります。

一般的に、シェーダープログラムにおいては条件演算子のほうが通常の If 文よりも高速であると言われます。 If 文は命令の流れ自体を切り替えてしまう(真の分岐)のに対して、条件演算子は条件式と、値として返すために引数として取った式のすべてを先に評価し、条件式の値に基づいて返す値を選択するという処理になる(ことが多い)ためです。 これであれば、実際には命令パイプライン上での分岐は発生していないため、値を得るための演算コストが分岐処理のコストを上回らない限りにおいては条件演算子が高速に動作します。

非常に曖昧な書き方をしましたが、これはコンパイラの最適化なども考慮に入れれば、GPU 上での処理とコードの記述が常にこのように対応するとは限らないためです。 少なくとも、単純な式を扱う条件演算子は前述のような挙動をすると考えて良いと思います。逆に、コンパイラの賢さにもよりますが、内部が非常に単純な If 文であれば、真の分岐は行わずに両方の分岐先を実行した上で値を選択する可能性もあります。

この辺りについては、OpenGL Wiki によい記述がありましたのでよければご参照ください。(英語)

Shader#Execution model and divergence - OpenGL Wiki


まとめのようななにか

処理速度の話はプラットフォームごとのアーキテクチャやコンパイラの賢さによって複雑な場合分けが発生するためなんとも言えませんが、マテリアルの If ノードは条件演算子に展開されるということだけはわかりました。

share