考虑一个经常调用的递归函数,其中一些参数在执行中变化很大,而另一些参数则没有,表示某种上下文信息。例如,树遍历可能看起来像这样
private void Visit(Node node, List<Node> results)
{
if (IsMatch(node)) {
results.Add(node);
}
Visit(node.Left, results);
Visit(node.Right, results);
}
...
Visit(root, new List<Node>());
显然,结果集合创建一次,并且在所有遍历调用中使用相同的引用。
问题是,无论函数是声明为Visit(Node, List<Node>)
还是Visit(List<Node>, Node)
,性能是否重要?参数顺序是否有约定?
模糊的想法是,固定参数可能不会被推入或不断地从堆栈中弹出,从而提高性能,但我不确定它是多么可行。
我主要对C#或Java感兴趣,但希望了解该订单所关注的任何语言。
注意:有时我碰巧总共有四个参数中的三个。我意识到可以创建一个包含上下文的类,或者一个关闭上下文变量的匿名函数,但问题是关于普通递归。
答案 0 :(得分:0)
这对我来说是最深刻的猜想领域,因为有很多可能影响结果的变量,从平台的调用约定到编译器的特性。
问题是,功能是否对性能有影响 被声明为访问(节点,列表)或访问(列表,节点)?是 有参数命令的约定吗?
在这种简单的例子中,我可以给出的最简洁的答案是,“除非另有证明否则” - “无罪直到被证实有罪”。这是非常不可能的。
但是有一些有趣的场景出现在一个更复杂的例子中:
API_EXPORT void f1(small a, small b, small c, small d, big e);
... vs:
API_EXPORT void f2(big a, small b, small c, small d, small e);
在这种情况下,f1
在某些情况下实际上可能比f2
更有效。
这是因为一些调用约定,比如Win x64,允许传递给函数的前四个参数直接通过寄存器传递,而不会发生堆栈溢出,只要它们适合寄存器并构成函数的前四个参数。 / p>
在这种情况下,使用f1
,前四个small
参数可能适合寄存器,但第五个参数(无论大小)需要溢出到堆栈。
对于f2
,第一个参数可能太大而无法放入寄存器中,因此需要将其与第五个参数一起溢出到堆栈中,这样我们最终可能会溢出两倍的变量到堆栈并获得一个小的性能命中(我从来没有真正测量过像这样的情况)。
这是一种罕见的情况,即使它发生了,并且我必须在那里放置API_EXPORT
说明符,因为除非导出此函数,否则编译器(或某些情况下的链接器)可以做各种各样的魔术并内联那种功能和事物,并且不需要以ABI定义的这种精确方式传递参数。
在这种情况下,让一些参数按照从寄存器中适合的最小类型到不适合的最大类型的升序排序可能会有所帮助。
但即便如此,List<Node>
是大型还是小型?它可以适用于通用寄存器,例如?像Java和C#这样的语言将所有引用UDT的内容视为对垃圾收集内存的引用。这可能归结为一个适合寄存器的简单存储器地址。我不知道引擎盖下发生了什么,但我怀疑引用本身可以放入寄存器中,实际上是一个小类型。一个大类型就像一个按值传递的4x4矩阵(作为深层复制)。
无论如何,这都是逐案的,并且有很多猜想,但这些是可能在某种罕见情况下以某种方式影响事物的潜在因素。
尽管如此,对于你引用的例子,我建议强烈的“否则,除非另有证明 - 非常不可能”。
我意识到可以创建一个包含上下文的类,或者 关闭上下文变量[...]的匿名函数。
通常,提供类的语言会生成机器指令,就好像函数中有一个额外的隐式参数一样。例如:
class Foo
{
public:
void f();
};
......可能会转化为类似的东西:
void f(Foo* this);
在考虑调用约定,别名以及如何通过寄存器和堆栈传递参数时,请记住这一点。
...但问题是关于普通递归
递归可能不会对此产生如此大的影响,除非编译器可能会停止内联递归函数,例如在几个等级之后,我们不可避免地要支付ABI所需的全部费用。但是否则它大致是相同的:函数调用是一个函数调用,无论它是否递归,并且相同的关注点往往适用于两者。这里的潜在相关事情很可能与编译器和平台ABI有很大关系,而不是递归地或非递归地调用函数。