为什么纹理查找比直接计算慢这么多?

7
我正在进行一个基于OpenGL的Oculus Rift畸变着色器实现。该着色器的工作原理是通过使用畸变系数对输入纹理坐标(其中包含先前渲染场景的纹理)进行变换,然后使用变换后的纹理来确定片元颜色。
我本希望通过预计算畸变并将其存储在第二个纹理中来提高性能,但结果实际上比直接计算更
直接计算的版本基本上看起来像这样:
float distortionFactor(vec2 point) {
    float rSq = lengthSquared(point);
    float factor =  (K[0] + K[1] * rSq + K[2] * rSq * rSq + K[3] * rSq * rSq * rSq);
    return factor;
}

void main()
{
    vec2 distorted = vRiftTexCoord * distortionFactor(vRiftTexCoord);
    vec2 screenCentered = lensToScreen(distorted);
    vec2 texCoord = screenToTexture(screenCentered);
    vec2 clamped = clamp(texCoord, ZERO, ONE);
    if (!all(equal(texCoord, clamped))) {
        vFragColor = vec4(0.5, 0.0, 0.0, 1.0);
        return;
    }
    vFragColor = texture(Scene, texCoord);
}

K是一个作为uniform传递的vec4。

另一方面,位移贴图查找看起来像这样:

void main() {
    vec2 texCoord = vTexCoord;
    if (Mirror) {
        texCoord.x = 1.0 - texCoord.x;
    }
    texCoord = texture(OffsetMap, texCoord).rg;
    vec2 clamped = clamp(texCoord, ZERO, ONE);
    if (!all(equal(texCoord, clamped))) {
        discard;
    }
    if (Mirror) {
        texCoord.x = 1.0 - texCoord.x;
    }
    FragColor =  texture(Scene, texCoord);
}

还有一些其他的操作用于校正宽高比和考虑镜头偏移,但它们都很简单。真的可以期望这样做比简单的纹理查找更加出色吗?

3个回答

12

GDDR内存延迟很高,现代GPU架构具备足够的数值计算能力。过去的情况恰好相反,GPU无法完成计算,规范化从立方体贴图中提取更加便宜。

此外,您所做的不是 常规 纹理查找,而是 相关 查找,这一点也不令人惊讶。由于您获取的位置取决于另一个获取的结果,因此您的着色器需要的内存无法进行预取或高效缓存(这是一种有效的延迟隐藏策略)。那就不是“简单的纹理查找”了。

而且,除了执行相关纹理查找之外,您的第二个着色器还包括 discard 关键字。这将有效地消除许多硬件上的早期深度测试可能性。

老实说,我不明白为什么要将 distortionFactor (...) 函数优化为查找。它使用了 平方长度,因此您甚至没有处理 sqrt,只需进行一堆乘法和加法。


虽然很有趣。从帧缓冲渲染纹理中获取实际片段颜色的查找总是有条件的。我从未在修改它们之前使用输入纹理坐标。但是,我想驱动程序可能足够聪明,可以预取靠近相邻片段源的输入像素,因为它在整个输出上移动..但是,对于使用坐标的纹理查找而不是计算的方法也是如此。 - Jherico
@Jherico:你是正确的,从技术上讲,任何计算纹理坐标而不是从阶段输入中获取的内容都被视为依赖纹理查找。然而,在现代GPU上,线程调度单元(NV硬件上的warp,AMD硬件上的wavefront)可以重新调度,以便在等待内存提取时进行有用的计算。相比计算坐标而必须进行纹理查找来查找第二个纹理查找的着色器将更频繁地停顿。 - Andon M. Coleman
@Jherico:GPU会尝试通过切换到另一个warp/wavefront来抵消这种情况,但它们很快也会停滞,因为在您的着色器中进行第一次纹理查找之前只能执行约1个计算。理想情况下,您希望在第一次纹理查找之前进行多个计算,以便在它们必须停止内存提取时,可以有warp/wavefront积极地处理着色器的不同部分。已经有很多关于这个主题的论文,现代GPU开始采用类似于CPU的多级缓存架构。 - Andon M. Coleman
哦,关于丢弃禁用早期深度测试的内容是一个很方便的知识点,但我应该指出Oculus Rift扭曲是将2D图像进行镜头校正。深度测试将完全关闭,因为所有渲染都在2D平面上工作。 - Jherico

6
Andon M. Coleman已经解释了正在进行的事情。实际上,现代GPU的主要瓶颈是内存带宽和更重要的内存延迟,因此在2007年至今构建的所有东西中,简单计算通常比纹理查找快得多。
事实上,内存访问模式对效率有如此大的影响,稍微重新排列访问模式并确保正确对齐可以轻松地提高1000倍的性能(BT;DT,然而这是CUDA编程)。依赖查找不一定会影响性能:如果依赖纹理坐标查找与控制器纹理是单调的,则通常不会太差。
说到这里,你从未听说过霍纳法则吗?你可以改写。
float factor =  (K[0] + K[1] * rSq + K[2] * rSq * rSq + K[3] * rSq * rSq * rSq);

trivially to

float factor =  K[0]  + rSq * (K[1] + rSq * (K[2] + rSq * K[3]) );

节省您几个操作。


如果着色器编译器/优化器没有自动重新排列表达式以利用霍纳规则,我会感到惊讶。但是明确地表达也没有什么不好的理由。 - Drew Hall
@DrewHall:实际上编译器无法优化这种情况。为了说明这一点,请将以下内容放入C编译器中:#include int main() { printf("%d %d %d\n", 5 * 10 / 20, (5*10)/20, 5*(10/20)); return 0; }并运行它。虽然它使用的是int而不是float,但它展示了同样的原则,即C风格编译器按运算符解析表达式,并将中间值作为下一个子表达式的实际L值。因此,尽管在纯数学上它们是相同的,但在C风格代码级别上,霍纳方法的收缩与扩展不同。 - datenwolf
我认为这在很大程度上取决于编译器本身和你传递的标志(例如,你告诉它如何处理浮点计算的严格程度)。我预期GPU编译器在浮点重排序方面会比较自由,而不是确保严格准确性。至于中间/L值,我同意对于编译器的解析器部分来说是正确的,但是优化器仍然可以通过控制流做很多事情而不改变可观察行为。 - Drew Hall
至少 NVIDIA 的 GLSL 编译器非常宽松,例如双单精度技巧如果不隐藏一些值以防止过于激进的优化则无法使用。 在这里可以看到我的黑客示例。NVIDIA 的编译器对 GLSL 所做的工作基本上就像 gcc 对 C、C++和其他面向 CPU 的语言所执行的 -ffast-math 一样。 - Ruslan

0

GPU是高度并行的,可以在单个时钟周期内计算多达1000个结果。内存读取始终是顺序的。如果需要5个时钟周期进行乘法计算,则可以在5个时钟周期内计算出1000个结果。如果数据必须按顺序读取,每个时钟周期有10个数据集,则获取数据需要100个时钟周期而不是5个。这里只是随机数字,以便您理解 :)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接