Shader中的 if 和分支

Shader中的 if 和分支

Shader 中提供了一些流程控制指令,如 if、for、while、switch、discard等。而使用最频繁的是以 if 为首的可能会产生分支的流程控制指令,所以本文也主要围绕 if 和分支进行讨论。

1.对 if 的传统理解

在 Shader 中,尽量避免使用 if 已成为绝大多数开发者的共识,究其原因是认为 if 会打断 GPU 的 warp内部(或者 wavefront,下文统称 warp)的并行化。

一种“优化”思路是规避 if 关键字,用内置指令替代,如 step 等。如:

if(cx > cy)
    x = a;
    x = b;
}

用内置指令优化一下:

lerp(a, b, step(cx, cy));

或者采取更“花哨”的内置函数组合封装一些通用判断方法,如这篇帖子: Avoiding Shader Conditionals 。以文中的 when_gt 示例:

float when_gt(float x, float y) {
  return max(sign(x - y), 0.0);
col += 0.5 * when_gt(IN.uv.x, IN.uv.y));

对应的汇编指令如下:

   0: add r0.x, -v0.y, v0.x
   1: lt r0.y, l(0.000000), r0.x
   2: lt r0.x, r0.x, l(0.000000)
   3: iadd r0.x, -r0.y, r0.x
   4: itof r0.x, r0.x
   5: max r0.x, r0.x, l(0.000000)
   6: lt r0.y, v0.y, v0.x
   7: movc r0.y, r0.y, l(0.600000), l(0.100000)
   8: mad o0.xyzw, r0.xxxx, l(0.500000, 0.500000, 0.500000, 0.500000), r0.yyyy
   9: ret 

对照 if 表达式:

if (x > y)
     col += 0.5; 
}

生成的汇编指令:

   0: lt r0.x, v0.y, v0.x
   1: movc o0.xyzw, r0.xxxx, l(0.600000,0.600000,0.600000,0.600000), l(0.100000,0.100000,0.100000,0.100000)
   2: ret 

"if 表达式" 版本只有1条比较指令和 movc 指令,而 "when_gt" 生成的指令数更多、成了负优化,执行效率上来说 if 表达式完胜。(当然每条指令在硬件上执行的 cycle 不同,如果对照的指令差别很大,则不能纯粹通过比较指令数来衡量最终的GPU执行效率。)

2. if ≠ 分支

从上面 if 生成的指令来看,if 没有生成分支指令。事实上,对于简单逻辑而言,编译器大多生成的是一条现代GPU会硬件支持的 "select"(或称为 "conditional move")指令,在D3D、PowerVR 等指令集中对应的就是一条 movc 指令。

所以,if 有可能生成分支,也有可能生成 "select"。if 不等于分支,我们真正要规避的是分支,而不是 if。if 最终是否会生成和执行分支指令,取决于具体厂商的“编译器 + driver + GPU”。像苹果就明确让开发者使用 ternary 操作符进行 "select",并且在A8及之后的 GPU 都对 ternary 提供硬件支持:

对于 "select" ,我们可以通过iq大佬给出的一段测试用例( "Select" Test ),比较下不同的写法和对应的性能表现:

// conditional move
#if METHOD==0
col = (p.x<h && p.y<h+h) ? col+tmp : col;
#endif
// "smart" way
#if METHOD==1
col += tmp*step(p.x,h)*step(p.y,h+h);
#endif
// even "smarter" way
#if METHOD==2
col += tmp*float(p.x<h && p.y<h+h);
#endif
// conditional branching
#if METHOD==3