【问题标题】:GLSL vertex shader performance with early return and branching具有早期返回和分支的 GLSL 顶点着色器性能
【发布时间】:2018-05-05 01:27:06
【问题描述】:

我有一个这样的顶点着色器

void main (){

    vec4 wPos = modelMatrix * vec4( position , 1. );

    vWorldPosition = wPos.xyz;

    float mask = step(
        0.,
        dot(
            cameraDir, 
            normalize(normalMatrix * aNormal)
        )
    );

    gl_PointSize = mask * uPointSize;

    gl_Position = projectionMatrix * viewMatrix * wPos;

}

我不完全确定如何测试着色器的性能,并排除其他因素,如过度绘制。我想一个大小为 1 的点,在屏幕空间中以网格形式排列,没有任何重叠会起作用吗?

否则我会对这些调整感到好奇:

(删除step,删除一个乘法,引入ifelse

void main (){

    if(dot(
         cameraDir, 
         normalize(normalMatrix * aNormal) //remove step
    ) < 0.) {
        gl_Position = vec4(0.,.0,-2.,.1); 
        gl_PointSize = 0.;
    } else {

        gl_PointSize = uPointSize; //remove a multiplication

        vec4 wPos = modelMatrix * vec4( position , 1. );

        vWorldPosition = wPos.xyz;
        gl_Position = projectionMatrix * viewMatrix * wPos;
    }

}

对比这样的:

void main (){

    if(dot(
         cameraDir, 
         normalize(normalMatrix * aNormal) 
    ) < 0.) {
        gl_Position = vec4(0.,.0,-2.,.1); 
        return;
    }

    gl_PointSize = uPointSize; 

    vec4 wPos = modelMatrix * vec4( position , 1. );

    vWorldPosition = wPos.xyz;

    gl_Position = projectionMatrix * viewMatrix * wPos;

}

这些着色器的行为会有所不同吗?为什么/如何?

如果有什么东西可以量化性能差异,我很感兴趣。

  • 是否有一些值,例如 MAD 的数量或不同代码显然会产生的其他值?
  • 不同代的 GPU 会以不同的方式处理这些差异吗?
  • 如果保证步骤版本最快,是否有一个已知的模式列表来说明如何避免分支,以及首选哪些操作? (比如使用floor 代替step 也可以?)

.

float condition = clamp(floor(myDot + 1.),0.,1.); //is it slower?

【问题讨论】:

  • 有一个明显的区别,即您修改后的着色器不会在所有路径上设置输出 (gl_pointSize/gl_Position),而原始着色器会在所有路径上设置输出,因此遇到这些情况的任何顶点都可能产生垃圾。
  • 啊,我没有意识到gl_PointSize 也很重要。我想我已经看到它在不同的浏览器上表现不同,有时是 0,这意味着规范没有定义它?
  • 我会编辑那个更有意义的,让返回的那个作为它自己的样本?
  • 第二个示例现在确实设置了两个输出,但在一个分支中的计算量较少。
  • 第二个示例似乎仍然在第一个if 中命中返回路径,而没有写入gl_PointSizegl_Position,因此仍然是非法的。

标签: opengl-es glsl webgl


【解决方案1】:

变量太多了,所以答案是“视情况而定”。一些 GPU 可以处理分支。有些不能,代码由编译器扩展,因此没有分支,只有乘以 0 的数学和不是的其他数学。然后是一些尝试积极避免过度绘制的平铺 GPU。我敢肯定还有其他因素。

理论上,您可以运行一百万次或几百万次着色器迭代,并使用

gl.readPixels(one pixel);
const start = performance.now();
...draw a bunch..
gl.readPixels(one pixel);
const end = performance.now();
const elapsedTime = end - start;

gl.readPixels 是一个同步操作,因此它会暂停 GPU 管道。 elapsedTime 本身并不是实际时间,因为它包括启动 GPU 并停止它等等,但似乎您可以将一个着色器中的 elapsedTime 与另一个着色器进行比较,看看哪个更快。

换句话说,如果elapsedTime 是 10 秒,这并不意味着您的着色器需要 10 秒。这意味着启动 gpu、运行着色器和停止 GPU 需要 10 秒。这些秒中有多少是开始的,有多少是停止的,有多少是你的着色器不可用的。但是,如果一个着色器的 elaspedTime 是 10 秒,另一个着色器是 11 秒,那么可以肯定地说一个着色器比另一个着色器快。请注意,您可能希望使您的测试足够长,以便获得几秒的差异,而不是微秒的差异。您还需要在多个 GPU 上进行测试,看看速度差异是否始终成立。

请注意,在顶点着色器中调用return 不会阻止生成顶点。事实上,gl_Position 在这种情况下是未定义的。

【讨论】:

  • 这是一个相关链接。我感觉我可能错过了对编译工作原理的基本理解。除了错过 GPU 的工作原理。这是一个公平的说法吗?这些会抑制对此的理解吗? "generally more expensive"
  • 使用 webgl,可以测量这一点,就像 webgl stats 测量跨设备/浏览器可用的扩展程序一样?编译一个包含大量示例的数据库并提出一些范围是不可能的吗? FOO 的行为就像这个 BAR 那样?
  • ...draw a bunch 应该画很多顶点吗?将 3 个矩阵乘法与 2 个进行比较,在 2.000.000 次迭代中得到非常相似的结果?这是一个 1080,这是否意味着绘图调用开销占用了运行它所需的 14 秒的大部分时间?多次实例化一个非常重的网格会更好吗?
【解决方案2】:

条件分支在 GPU 上的开销很大——通常比乘法开销要大得多,因此修改后的着色器可能会更慢。

【讨论】:

  • 是否可以详细说明到底发生了什么?不同的 GPU 是否根据世代或其他因素来处理这种情况?是否可以从中派生出许多指令(x 个 MAD,或其他)?
  • 我已经修改了问题。
  • float result = foo * mask.x + bar * mask.y;if(mask.x &gt; 0.5) { result = foo; } else { result = bar; }
猜你喜欢
  • 1970-01-01
  • 2011-08-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-08-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多