开始学习着色器后,我读到的第一件事是出于性能原因,必须尽可能避免(动态)条件分支:显然两个分支都将运行,然后根据条件。
但是,在查看示例着色器时,我发现了这个autostereogram shader on Shadertoy。在主函数的第30行,我们可以看到:
for(int count = 0; count < 100; count++) {
if(uv.x < pWid)
break;
float d = getDepth(uv);
//d = 1.;
uv.x -= pWid - (d * maxStep);
}
在这里,我们在for
循环中有条件地中断。天真的,基于上面的“无条件分支”,人们会希望它具有糟糕的性能,因为每个循环出现一个分支(此处为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
会使循环变大,但速度会变慢,虽然幅度不大。)
因此,如果它没有运行所有可能的条件分支,那么这里到底发生了什么?在什么情况下可以使用动态分支而不会降低性能?
答案 0 :(得分:3)
使用现代GPU,将需要着色的(在这种情况下)像素的总工作量划分为图块/组,然后通过“扭曲” /“波前”¹处理图块,“扭曲” /“波前”¹是一组在其中运行的线程锁步是指处理一个图块的扭曲中的所有线程都运行相同的指令,但使用不同的数据(SIMD)。
想象一下,一个扭曲处理2x2像素,您的三个像素需要10次迭代,但是第四个像素需要100次迭代,因此所有线程都运行100次迭代,前三个像素多余的90次迭代的结果将被“遮罩” //丢弃,因此它不会影响它们的输出,但是只有在所有线程都完成处理后,整个扭曲才会继续移动到下一个像素。但是,当所有线程在10次迭代后退出时,扭曲可能会缓解并继续前进,因此您将获得性能上的提升。
您可以找到对以上here的(略)冗长的解释。
瞥见现代GPU here上的调度/平铺的内部工作。
¹“ warp”是NVIDIA,“ wavefront” AMD术语