让我们看一下GLSL中的简化示例函数:
void foo() {
vec2 localData[16];
// ...
int i = ... // somehow dependent on dynamic data (not known at compile time)
localData[i] = x; // THE IMPORTANT LINE
}
它将一些值x
写入本地数组中的动态确定索引。
现在,用
localData[i] = x;
for( int j = 0; j < 16; ++j )
if( i == j )
localData[j] = x;
使代码显着更快。在几个经过测试的示例(不同着色器)中,执行时间几乎减半,并且还有比这次写入更多的事情。
例如:在一个与顺序无关的透明度着色器中,除其他外,它使用直接写入39ms
和带有循环写入的23ms
来获取16个纹素。没有其他改变!
测试硬件是GTX1080。 glGetProgramBinary
返回的程序集仍然过高。它在第一种情况下包含一行,如果在第二种情况下包含相同的行,则包含循环+。
猜猜:localData
存储在8个vec4寄存器中(程序集没有说明任何内容)。我还假设,寄存器不能用索引来寻址。如果两者都为真,那么最终的二进制必须使用一些分支构造。循环变体可能会展开并导致类似switch
的模式更快。但这对所有供应商来说都很常见吗?为什么编译器不能使用来自for
循环的结果作为此类写入的默认值?
答案 0 :(得分:1)
进一步的实验表明,原因是对数组使用不同的内存类型。 (展开的)循环变体使用寄存器,而随机访问变体切换到本地存储器。
本地内存通常放在全局内存中,但对每个线程都是私有的。可能会缓存对此本地数组的访问(L2?)。
验证这种推理的实验如下:
展开循环的手动版本(以插入排序方式测量,包含超过1M像素的16个元素):
基线:localData[i] = x
33ms
for循环:for j + if i=j
16.8ms
开关:switch(i) { case 0: localData[0] ...
:16.92ms
如果是树(分成两半):16.92ms
如果列表(普通手动展开):16.8ms
=&GT;各种分支结构导致或多或少相同的时序。因此,最初的猜测并不是一个糟糕的分支行为。
多对一对比无随机访问(32元素插入排序)
2x localData[i] = x
47ms
1x localData[i] = x
45ms
0x localData[i] = x
16ms
=&GT;只要至少有一个随机访问,性能就会很糟糕。这意味着有一个全局决定改变localData
的行为 - 很可能是使用不同的内存。由于缓存,使用多个随机访问不会使事情变得更糟。