着色器函数参数性能

13

我在尝试理解着色器语言中参数传递的实现方式。

我已经阅读了几篇文章和文档,但仍然有些疑问。特别是,我正在尝试理解与 C++ 函数调用之间的区别,并着重关注性能方面的问题。

HLSL、CgGLSL 之间存在一些细微差异,但我想底层实现相当相似。

目前我所理解的:

  • 除非另有规定,否则函数参数始终是按值传递的(即使是矩阵也是如此?)
  • 在这种情况下,按值传递并不具有与 C++ 相同的含义。不支持递归,因此不使用栈,并且大多数函数都是内联的,参数直接放入寄存器中。
  • 默认情况下,函数通常都是内联的(HLSL),或者至少内联关键字总是受编译器尊重的(Cg

以上观点是否正确?

现在有两个具体问题:

  1. 将矩阵作为函数参数传递

    inline float4 DoSomething(in Mat4x4 mat, in float3 vec) { ... }

考虑上面的函数,在 C++ 中,这将是可怕的,最好使用引用:const Mat4x4&

那么在着色器中呢?这是一种不好的方法吗?我读到过例如可以使用 inout 限定符来按引用传递矩阵,但实际上它意味着被调用的函数将修改该矩阵..

  1. 参数数量(和类型)是否有任何影响?例如,使用具有有限参数集的函数是否更好?或避免传递矩阵? 在这里,inout 修改器是否是提高性能的有效方法?如果是,任何人都知道典型编译器如何实现此操作吗?

  2. 在这方面,HLSLGLSL 之间有何区别? 有人对此有什么提示吗?

3个回答

13

根据规范,值总是被复制。对于 in 参数,在调用时被复制,对于 out 参数,在返回时被复制,在调用和返回时都复制 inout 参数。

按照规范的术语(GLSL 4.50,第6.1.1节“函数调用约定”):

 

所有参数都在调用时按顺序从左到右精确评估一次。评估输入参数会得到一个被复制到形式参数的值。评估输出参数会得到一个 l-value,该 l-value 在函数返回时用于复制值。评估 inout 参数会得到一个值和一个 l-value;值在调用时被复制到形式参数,lvalue 在函数返回时用于复制值。

当然,实现可以自由优化任何它想要的,只要结果与文档中记录的行为相同即可。但我认为您不能指望它以任何特定的方式工作。

例如,通过引用传递所有inout参数是不安全的。假设有以下代码:

vec4 Foo(inout mat4 mat1, inout mat4 mat2) {
    mat1 = mat4(0.0);
    mat2 = mat4(1.0);
    return mat1 * vec4(1.0);
}

mat4 myMat;
vec4 res = Foo(myMat, myMat);

正确的结果是一个包含所有 0.0 组件的向量。如果参数是通过引用传递的,那么 Foo() 中的 mat1mat2 将引用同一个矩阵。这意味着对 mat2 的赋值也会改变 mat1 的值,结果将是一个所有分量均为 1.0 的向量,这是错误的。

当然,这只是一个非常人为的例子,但优化必须具有选择性才能在所有情况下正常工作。


感谢您的回答。所以可以确定inout不对应于按引用传递。我在Stack Overflow上读到过它会作为引用工作,但是您的示例表明这显然是错误的,并且不会优化任何内容。 - Heisenbug
1
@Heisenbug:你会在Stack Overflow和其他无数地方看到这样的讨论。inout并不是一种性能优化,而是GLSL中最接近引用的东西。实际上,正确的行为是在调用时复制数据,在返回时复制数据,但是优化编译器不会浪费时间进行不必要的复制,除非你像这里所示那样做某些事情。这回到了我在答案中讨论的内容,限定符本身并不重要,而是你传递的变量所做的事情真正重要。 - Andon M. Coleman
一个聪明的编译器通过引用、内联或任何它认为聪明的方式来完成。一个聪明的编译器也能理解 Foo(myMat, myMat) 的意图。对于我们来说,聪明的编译器是一个黑盒子。但不要太认真地认为编译器会“复制”mat4的实例,因为规范并没有这样说。 - Chameleon

4
您在考虑使用 inout 限定的参数时,第一个要点将不会起作用。
真正的问题在于您在函数内部对参数的处理方式。如果您修改了一个以 in 修饰的参数,则它不能被“按引用传递”,必须制作一份副本。在现代硬件上,这可能并不是什么大问题,但 Shader Model 2.0 在临时寄存器数量方面相当有限,当 GLSL 和 Cg 刚出现时,我遇到过这类问题不止一次。
请参考以下 GLSL 代码:
vec4 DoSomething (mat4 mat, vec3 vec)
{
  // Pretty straight forward, no temporary registers are required to pass arguments.
  return vec4 (mat [0] + vec4 (vec, 0.0));
}

vec4 DoSomethingCopy (mat4 mat, vec3 vec)
{
  mat [0][0] = 0.0; // This requires the compiler to make a local copy of mat
  return vec4 (mat [0] + vec4 (vec, 0.0));
}

vec4 DoSomethingInOut (inout mat4 mat, in vec3 vec)
{
  mat [0][0] = 0.0; // No copy required, but the original mat is modified
  return vec4 (mat [0] + vec4 (vec, 0.0));
}

我不能对性能发表评论,我的不好经验只与在较旧的GPU上达到实际硬件限制有关。当然,您应该假设每次需要复制某些内容时,都会对性能产生负面影响。


只要我不修改作为参数传递的矩阵,就不需要进行复制,编译器会优化吗? - Heisenbug
@Heisenbug:我可以明确地说,在HLSL中是这种情况(Microsoft实现了该编译器,如果您想查看它如何优化某些内容,可以查看生成的字节码)。GLSL则不同,因为每个供应商都实现了自己的编译器。虽然几乎无法想象供应商的编译器不会识别这种情况,但更奇怪的事情已经发生过。 - Andon M. Coleman
感谢提供的信息。我主要使用Profile 2.0针对移动设备进行开发(通过Unity,因此Cg被交叉编译为GLSL,使得事情更加难以预测)。我仍然会让问题保持开放状态一段时间,以便收集尽可能多的信息。 - Heisenbug
@Heisenbug:请保持开放状态。我最初能够将那个答案的要点放入一个评论中(去掉代码示例),这通常是一个不完整的回答的标志_(至少对我而言)_ ;) 如果您将图形API作为标签之一,您的问题可能会获得更多关注。 - Andon M. Coleman
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - jozxyqk
显示剩余3条评论

2
所有着色器函数都是内联的(禁止递归函数)。引用/指针的概念在此处也无效。唯一的情况是当您对输入参数进行写入时,会生成一些代码。但是,如果原始寄存器不再使用,则编译器可能会使用相同的寄存器,并且不需要复制(mov操作)。
总之:函数调用是免费的。

1
你有这个声明的来源吗?我没有看到规范中说实现必须内联所有函数。 - Reto Koradi
1
我猜规格没有说明这一点。但是,由于规格指出不允许递归,编译器可以自由地这样做。此外,我读过一些在 Nvidia 硬件上的 GLSL 着色器的汇编代码,所有函数都被内联:没有函数调用,也没有函数定义。我猜 AMD 硬件也会这样做。如果禁止递归函数,我不知道不这样做的原因。 - dv1729

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