本日はShader枠です。
Shaderの処理内でif文は避けるべきということはよく言われることですが、ComputeShaderでの実装の場合やなぜそもそもそのように言われているのか理解をしていないところがあったため今回は学んでいきます。
〇Shader内でIF分の処理を行うこと
Shader内でIF分を使用することについてはたくさんの私よりも強つよな方々か書かれた記事があります。
https://qiita.com/up-hash/items/e932ccae110d687e225f
先人の知識によると一概にIf文を使用することでパフォーマンスの悪化が発生するわけではないようです。
具体的にはGPUでの処理は、その並列演算性(マルチスレッド)が強みです。
これらのスレッドは、ワープと呼ばれるグループに分けられ、通常32スレッドが1ワープを構成します。
この時、あるスレッドではA、別のスレッドではBという条件がif文内で発生する場合、ワープ内のスレッドが異なる実行パスを取ることがあり、パフォーマンスが悪化してしまうのです。
つまり、If文の条件がスレッド内すべてでTrueもしくはFalseである場合は問題がなく、If文の条件がスレッド内で分岐する場合は負荷がかかってしまいます。
この負荷をスレッドダイバージェンスと呼びます。
〇スレッドダイバージェンス(Thread Divergence)とは?
スレッドダイバージェンスは前述の通りGPU処理内で複数のスレッドが異なる命令を実行し始めることを指します。
具体的な例は以下になります。
[numthreads(256, 1, 1)] void CSMain(uint3 id : SV_DispatchThreadID) { uint index = id.x; float3 position = vertexBuffer[index]; if (position.x > threshold) { position *= 2; // 条件を満たすスレッドが実行 } else { position += 1; // 条件を満たさないスレッドが実行 } vertexBuffer[index] = position; }
この場合同じワープ内のスレッドが異なる分岐をたどることになります。
GPUはワープ単位で命令を実行しており、異なる分岐をたどるスレッドがあると、全ての分岐が完了するまで他のスレッドが待機する必要があります。
この待機時間が増えることで、全体のパフォーマンスが低下します。
つまり一番遅い処理が終わるまでほかのスレッドも待ってしまうため、効率が低下してしまうということです。
逆にスレッドダイバージェンスが発生した場合でも分岐が短く軽量な場合はその影響は無視できるレベルということができます。
例えば下記のような条件が定数やすべての分岐においてどちらかに入る場合は影響が少ないということです。
int result =0; [numthreads(256, 1, 1)] void CSMain(uint3 id : SV_DispatchThreadID) { uint index = id.x; float3 position = vertexBuffer[index]; if (result==0) { position *= 2; // 条件を満たすスレッドが実行 } else { position += 1; // 条件を満たさないスレッドが実行 } vertexBuffer[index] = position; }
GPU上で実行されている以上スレッドダイバージェンスはコンピュートシェーダーにおいても発生することがあります。
近年ジオメトリシェーダーがコンピュートシェーダーに置き換えられる傾向があります。
これはたくさんの理由がありますが、その一つにジオメトリシェーダーでは、処理するプリミティブごとに異なる動作をする可能性があるため、シェーダーダイバージェンスが発生しやすいという理由があります。
例えば、ジオメトリシェーダーが三角形、線分、点といった異なるプリミティブを扱う場合、それぞれのプリミティブごとに異なる処理を行うとワーフ(Warp/Wavefront)内での分岐が発生します。
またジオメトリシェーダーはプリミティブごとに実行され頂点数やプリミティブ数が少ないとGPUリソースが十分に活用されません。
コンピュートシェーダーは任意のスレッドグループサイズを定義可能であり、頂点やプリミティブ単位での並列処理が柔軟に行えます。
これはスレッドダイバージェンスを避けて効率的な処理を行う過程において重要な取り組みと言えます。
本日は以上です。