着色器 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 术语
开始学习着色器后,我读到的第一件事是出于性能原因必须尽可能避免(动态)条件分支:显然两个分支都是 运行,然后根据条件选择结果。
但是,在查看示例着色器时,我发现了这个 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 术语