着色器 for 循环中的条件中断性能

Conditional break performances in for loop for shaders

开始学习着色器后,我读到的第一件事是出于性能原因必须尽可能避免(动态)条件分支:显然两个分支都是 运行,然后根据条件选择结果。

但是,在查看示例着色器时,我发现了这个 autostereogram shader on Shadertoy。在第30行,在main函数中,我们可以看到:

for(int count = 0; count < 100; count++) {
    if(uv.x < pWid)
        break;

    float d = getDepth(uv);
    //d = 1.;

    uv.x -= pWid - (d * maxStep);
}

在这里,我们在 for 循环中有一个条件中断。天真地,基于上面的 "no conditional branching",人们会期望它有糟糕的性能,因为每个循环发生都有一个分支(这里是 100)。然而,这种情况并非如此。事实上,将最大循环数从 100 增加到任何巨大的数字对性能没有明显影响。

我们可以取消分支,例如使用以下代码:

for(int count = 0; count < 100; count++) {
    float d = getDepth(uv);
    //d = 1.;

    uv.x -= (pWid - (d * maxStep)) * step(0.0, uv.x-pWid);
}

但随后性能会受到更大循环的影响:在 1000 或 10000 时,它会慢得像爬行一样。

(类似地,将 break 替换为 continue 会随着更大的循环而变慢,尽管不会那么多。)

所以如果它不是运行所有可能的条件分支,那么这里到底发生了什么?在哪些情况下我可以使用动态分支而不会影响性能?

对于现代 GPU,需要着色的(在本例中)像素的总工作负载被分成 tiles/groups,然后由“warp”/“wavefront”¹ 处理图块,这是一组在 lock-step 中 运行 的线程意味着处理 tile 运行 相同指令但具有不同数据的 warp 中的所有线程 (SIMD)。

假设有一个处理 2x2 像素的扭曲,其中三个像素需要 10 次迭代,但第四个需要 100 次,因此所有线程 运行 100 次迭代,这是第一个多余的 90 次迭代的结果三个像素将被“屏蔽”/丢弃,因此它不会影响它们的输出,但整个扭曲可能只会在所有线程完成处理后移动到下一个像素。但是,当所有线程在 10 次迭代后退出时,warp 可能会退出并更早地继续运行,因此您将获得性能提升。

您可以找到以上内容的(稍微)更长的解释 here

了解现代 GPU 上调度/平铺的内部工作原理 here

¹ “warp”是 NVIDIA,“wavefront”是 AMD 术语