着色器中分支的效率

时间:2010-11-14 04:56:35

标签: performance branch shader

我知道这个问题可能看起来有点没有根据,但如果有人在这个主题上知道任何理论/实践经验,那么分享它会很棒。

我正在尝试优化我的一个旧着色器,它使用了大量的纹理查找。

对于三个可能的映射平面中的每一个,我都有漫反射,普通,高光贴图,对于一些靠近用户的面,我还必须应用贴图技术,这也带来了很多纹理查找(如parallax occlusion mapping)。

分析显示纹理查找是着色器的瓶颈,我愿意将其中一些删除掉。对于输入参数的某些情况我已经知道纹理查找的一部分是不必要的,明显的解决方案是做(伪代码)

if (part_actually_needed) {
   perform lookups;
   perform other steps specific for THIS PART;
}

// All other parts.

现在 - 问题就出现了。

我不记得确切(这就是为什么我说这个问题可能没有根据),但在某篇论文中我最近读过(遗憾的是,记不起名字)陈述了与以下类似的内容:

  

所呈现的表现   技术取决于效率如何    基于硬件的条件   分支 已实施。

在我即将开始重构大量着色器并实现我正在谈论的基于 if的优化之前,我记得这种说法。

所以 - 就在我开始这样做之前 - 有人知道着色器中分支效率吗?为什么分支会在着色器中造成严重的性能损失?

甚至有可能我只能通过基于if的分支来恶化实际效果?


你可能会说 - 试试看。是的,如果这里没有人帮助我,那就是我要做的事情。)

但是,if案例中对于新GPU来说可能有效的东西对于一些较老的GPU来说可能是一场噩梦。 除非你有很多不同的GPU(这不是我的情况),否则这个问题很难预测

所以,如果有人对此有所了解或有这些着色器的基准测试经验,我将非常感谢您的帮助。


实际工作的剩余脑细胞几乎没有告诉我,GPU上的分支可能远不如分支CPU(通常具有非常有效的分支预测方法和消除缓存未命中)那么有效,仅仅因为它是GPU (或者在GPU上很难/不可能实现)。

不幸的是,我不确定这句话是否与实际情况有任何共同点......

5 个答案:

答案 0 :(得分:30)

如果条件是统一的(即整个传递的常量),那么分支基本上是免费的,因为框架将基本上编译着色器的两个版本(分支采用而不是)并为整个传递选择其中一个在你的输入变量上。在这种情况下,请务必使用if语句,因为 会使着色器更快。

如果条件因顶点/像素而异,那么它确实会降低性能,旧的着色器模型甚至不支持动态分支。

答案 1 :(得分:30)

不幸的是,我认为这里真正的答案是在目标硬件上使用特定情况的性能分析器进行实际测试。特别是考虑到你处于项目优化阶段;这是考虑硬件频繁更改和特定着色器性质这一事实的唯一方法。

在CPU上,如果你得到一个错误预测的分支,你将导致管道刷新,并且由于CPU管道太深,你将有效地丢失大约20个或更多周期的东西。在GPU上的东西有点不同;管道可能会更浅,但没有分支预测,所有着色器代码都将在快速内存中 - 但这并不是真正的区别。

很难知道正在发生的所有事情的确切细节,因为nVidia和ATI相对守口如瓶,但关键是GPU是为大规模并行执行而制作的。有许多异步着色器核心,但每个核心再次设计为运行多个线程。我的理解是每个核心都希望在任何给定周期内对所有线程运行相同的指令(nVidia将这个线程集合称为“warp”)。

在这种情况下,线程可能表示顶点,几何元素或像素/片段,而warp是其中大约32个的集合。对于像素,它们可能是屏幕上彼此接近的像素。问题是,如果在一个warp中,不同的线程在条件跳转时做出不同的决定,则warp已经发散并且不再为每个线程运行相同的指令。硬件可以处理这个问题,但它(至少对我来说)它是如何做到的并不完全清楚。对于每一代牌,它的处理方式也可能略有不同。最新,最通用的CUDA /计算着色器友好型nVidias可能具有最佳实现;较旧的卡可能有较差的实现。最糟糕的情况是你可能会发现许多线程执行if / else语句的两面。

着色器的一个重要技巧是学习如何利用这种大规模并行范式。有时这意味着使用额外的通道,临时的屏幕外缓冲区和模板缓冲区来将逻辑推出着色器并进入CPU。有时优化可能会燃烧更多周期,但实际上可能会减少一些隐藏的开销。

另请注意,您可以将DirectX着色器中的if语句明确标记为[branch]或[flatten]。展平样式为您提供正确的结果,但始终在说明中执行所有操作。如果你没有明确地选择一个,编译器可以为你选择一个 - 并且可以选择[flatten],这对你的例子没有好处。

要记住的一件事是,如果跳过第一个纹理查找,这将混淆硬件的纹理坐标派生数学。您将遇到编译器错误,最好不要这样做,否则您可能会错过一些更好的纹理支持。

答案 2 :(得分:25)

在许多情况下,可以通过条件计算和混合两个分支作为插值器。 这种方法比分支更快。也可以在CPU上使用。 例如:

...

vec3 c = vec3(1.0, 0.0, 0.0); if (a == b) c = vec3(0.0, 1.0, 0.0);

可以替换为:

vec3 c = mix(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0), (a == b));

...

答案 3 :(得分:10)

以下是点燃Fire的真实世界性能基准:

在片段着色器中......

以20fps运行:

lowp vec4 a = vec4(0.0, 0.0, 0.0, 0.0);
if (a.r == 0.0)
    gl_FragColor = texture2D ( texture1, TextureCoordOut );   

运行速度为60fps:

gl_FragColor = texture2D ( texture1, TextureCoordOut );   

答案 4 :(得分:7)

我不知道基于if的优化,但是如何创建你认为你需要的纹理查找的所有排列,每个都有自己的着色器,只需使用右边的着色器情况(取决于哪个纹理查找特定模型或模型的一部分,需要)。我想我们在Bully for Xbox 360上做过类似的事情。