GLSL:为什么本地数组中的随机写入比循环写入慢得多?

Joh*_*sie 5 performance gpu glsl compiler-optimization

让我们看一下 GLSL 中的简化示例函数:

void foo() {
    vec2 localData[16];
    // ...
    int i = ... // somehow dependent on dynamic data (not known at compile time)
    localData[i] = x; // THE IMPORTANT LINE
}
Run Code Online (Sandbox Code Playgroud)

它将一些值写入x本地数组中动态确定的索引。现在,将该行替换localData[i] = x;

for( int j = 0; j < 16; ++j )
    if( i == j )
        localData[j] = x;
Run Code Online (Sandbox Code Playgroud)

使代码明显更快。在几个测试示例(不同的着色器)中,执行时间几乎减半,并且发生的事情比此写入要多得多。

例如:在与顺序无关的透明度着色器中,除其他外,它获取 16 个纹素,计时采用39ms直接写入和23ms循环写入。其他什么都没变!

测试硬件是GTX1080。返回的程序集glGetProgramBinary仍然太高级。它在第一种情况下包含一行,在第二种情况下包含一个循环+if 围绕相同的行。

  • 为什么会出现这个性能问题?
  • 对于所有供应商来说都是如此吗?

猜测:localData存储在 8 个 vec4 寄存器中(程序集没有说明这一点)。此外,我假设寄存器不能用索引来寻址。如果两者都为真,则最终的二进制文件必须使用某种分支构造。循环变体可能会展开并导致switch更快的类似模式。但这对于所有供应商来说都是常见的吗?为什么编译器不能使用循环的任何结果for作为此类写入的默认结果?

Joh*_*sie 3

进一步的实验表明,原因是数组使用了不同的内存类型。(展开的)循环变体使用寄存器,而随机访问变体切换到本地内存

本地内存通常放置在全局内存中,但对每个线程都是私有的。对该本地数组的访问很可能会被缓存(L2?)。


验证这一推理的实验如下:

  1. 展开循环的手动版本(以超过 100 万像素的 16 个元素的插入排序进行测量):

    基线:localData[i] = x 33ms
    For 循环:for j + if i=j 16.8ms
    开关switch(i) { case 0: localData[0] ...::16.92ms
    If else 树(分成两半):16.92ms
    If 列表(普通手册展开):16.8ms

    => 各种分支结构都会导致或多或少相同的时序。所以这并不是像最初猜测的那样是一个糟糕的分支行为。

  2. 多个、一个、无随机访问(32 元素插入排序)

    2x localData[i] = x47 毫秒
    1x localData[i] = x45 毫秒
    0x localData[i] = x16 毫秒

    => 只要有至少一次随机访问,性能就会很差。这意味着有一个全局决策改变了行为localData——很可能是使用不同的内存。由于缓存的存在,使用多个随机访问不会使情况变得更糟。