【Unity】【Shader】stepの分岐と三項演算子のコンパイル結果を見てみた

# 気になった投稿
https://x.com/iquilezles/status/1888409333182218691

> ちょっと、 mix () +step ()で条件付き移動を「最適化」しないでください。そのアドバイスはやめてください。理由: 

https://iquilezles.org/articles/gpuconditionals/

ifを書くのを避けるテクニックとして、lerpとstepを使用する方法があります。
※iq氏の投稿はGLSLのためmixとなっている

# なぜか
`step`を使用すると
float step( float x, float y )
{
    return x < y ? 1.0 : 0.0;
}
このように三項演算子と0.0と1.0が生成される？ようです。見た目では三項演算子を使用しないコードを書いたにも関わらず、結局内部で三項演算子が出現するため無駄のようです。

# 試してみる
``` HLSL
float4 frag(Varyings i) : SV_Target
{
    float4 col = float4(0.0, 0.0, 0.0, 1.0);
    
    float c = lerp(0.0, 1.0, step(0.5, i.uv.x));
    //float c = i.uv.x >= 0.5 ? 1.0 : 0.0;

    col.rgb = c;
    col.a = 1.0;

    return col;
}
```
こちらのコードを使用します。
uvのxによって色を変更します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/608797/c21ea210-15b3-433b-8b15-191f9139812d.png)

結果はこのような画像です。
今回重要なのは出力結果の絵ではなく、内部です。そのためコンパイル結果を見ます。
Shaderを選択してCompileボタンを押せば見れます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/608797/f5ea2fa3-3d9a-4e51-a3a9-0a94c73e66ab.png)

まずは`lerp(0.0, 1.0, step(0.5, i.uv.x))`を行った結果です。
``` c
0: ge r0.x, v1.x, l(0.500000)
1: and o0.xyz, r0.xxxx, l(0x3f800000, 0x3f800000, 0x3f800000, 0)
2: mov o0.w, l(1.000000)
3: ret 
```

`ge`では`greater or equal`を行います。入力値が0.5より大きいかを比較しています。
`and`ではANDを行います。`ge`で0か1が来るのでそれを使用して比較します。
`mov`ではw(アルファ値)に1.0を入れています。

次は`float c = i.uv.x > 0.5 ? 1.0 : 0.0`の処理です。

``` c
0: lt r0.x, l(0.500000), v1.x
1: and o0.xyz, r0.xxxx, l(0x3f800000, 0x3f800000, 0x3f800000, 0)
2: mov o0.w, l(1.000000)
3: ret
```
stepを使用したときとほぼ同じですね。
`lt`はless thanのようなのでstepは以上かを知らべているのでそちらに合わせたコードにしてもう一度コンパイル結果を見ます。

`float c = i.uv.x >= 0.5 ? 1.0 : 0.0`の処理です。

``` c
0: ge r0.x, v1.x, l(0.500000)
1: and o0.xyz, r0.xxxx, l(0x3f800000, 0x3f800000, 0x3f800000, 0)
2: mov o0.w, l(1.000000)
3: ret 
```
同じになりました。

つまり、lerpとstepを使って最適なコードにしているように思えて内部的に行っているコードは三項演算子使って分岐するのと変わらないようです。(今回のコードでは)

# iq氏のサイトに載っていたコードをコンパイルしてみる

三項演算子を使用したコード
``` HLSL
float2 snap45( in float2 v )
{
    float2 s = sign(v);
    float x = abs(v.x);
    return x>0.923880?float2(s.x,0.0):
           x>0.382683?s*sqrt(0.5):
                    float2(0.0,s.y);
}
```
``` C
0: lt r0.xy, l(0.000000, 0.000000, 0.000000, 0.000000), v1.xyxx
1: lt r0.zw, v1.xxxy, l(0.000000, 0.000000, 0.000000, 0.000000)
2: iadd r0.xy, -r0.xyxx, r0.zwzz
3: itof r0.xy, r0.xyxx
4: mul r1.xy, r0.xyxx, l(0.707107, 0.707107, 0.000000, 0.000000)
5: lt r1.zw, l(0.000000, 0.000000, 0.923880, 0.382683), |v1.xxxx|
6: mov r0.z, l(0)
7: movc r0.yw, r1.wwww, r1.xxxy, r0.zzzy
8: movc o0.xy, r1.zzzz, r0.xzxx, r0.ywyy
9: mov o0.zw, l(0,0,0,1.000000)
10: ret 
```
比較やmovを行っているようで分岐は無いようです。

次は**最適化したように見える**コードです。
``` HLSL
float2 snap45( in float2 v )
{
    float2 s = sign(v);
    float x = abs(v.x);

    float w0 = step(0.92387953,x);
    float w1 = step(0.38268343,x)*(1.0-w0);
    float w2 = 1.0-w0-w1;

    float2 res0 = float2(s.x,0.0);
    float2 res1 = float2(s.x,s.y)*sqrt(0.5);
    float2 res2 = float2(0.0,s.y);

    return w0*res0 + w1*res1 + w2*res2;
}
```
``` C
0: mov r0.y, l(0)
1: lt r0.zw, l(0.000000, 0.000000, 0.000000, 0.000000), v1.xxxy
2: lt r1.xy, v1.xyxx, l(0.000000, 0.000000, 0.000000, 0.000000)
3: iadd r0.zw, -r0.zzzw, r1.xxxy
4: itof r1.xy, r0.zwzz
5: ge r0.zw, |v1.xxxx|, l(0.000000, 0.000000, 0.923880, 0.382683)
6: movc r1.w, r0.z, l(0), l(1.000000)
7: and r0.zw, r0.zzzw, l(0, 0, 0x3f800000, 0x3f800000)
8: mul r2.x, r1.w, r0.w
9: mad r0.w, -r0.w, r1.w, r1.w
10: mul r0.x, r1.x, r0.z
11: mul r1.xw, r1.xxxy, r2.xxxx
12: mad r0.xy, r1.xwxx, l(0.707107, 0.707107, 0.000000, 0.000000), r0.xyxx
13: mov r1.z, l(0)
14: mad o0.xy, r0.wwww, r1.zyzz, r0.xyxx
15: mov o0.zw, l(0,0,0,1.000000)
16: ret 
```
命令数が増えています。
Stepを使ったせいで`and`や他の計算のために`mad`が出てきており、増えています。

# まとめ
この結果から、lerp+stepによる方法は別に最適化されているわけではないことがわかりました。