内联递归函数有什么权衡。
答案 0 :(得分:12)
可以通过尾端递归优化的递归函数当然可以内联。如果函数做的最后一件事就是调用自身,那么它就可以转换成一个普通的循环。
答案 1 :(得分:9)
任何递归函数都无法内联,因为蛇不能吞下自己的尾巴。
答案 2 :(得分:7)
[编辑:只是注意到虽然你的标题上写着“内联”,但你的实际问题却是“使函数内联”。这两者实际上没有任何关系,他们只是有着令人困惑的相似名字。在现代编译器中,inline
的主要影响是C99中最初的东西(我认为)只是内联工作的必要细节:允许带有外部链接的符号的多个定义。这是因为现代编译器并没有全神贯注程序员对是否应该内联函数的看法。但是,他们付出了一些代价,因此概念的混乱仍然存在。我在标题中回答了问题,这是编译器做出的决定,而不是正文中的问题,这是程序员做出的决定。]
内联不一定是一个全有或全无的交易。编译器用来决定是否内联的一种策略是在结果代码“太大”之前保持内联函数调用。 “大”是由一些有希望的明智的启发式定义的。
因此,请考虑以下递归函数(故意不是简单的尾递归):
int triangle(int n) {
if (n == 1) return 1;
return n + triangle(n-1);
}
如果这样称呼:
int t100() {
return triangle(100);
}
然后,原则上没有特别的理由认为编译器用于内联的通常规则不应该导致:
int t100() {
// inline call to triangle(100)
int result;
if (100 == 1) { result = 1; } else {
// inline call to triangle(99)
int t99;
if (100 - 1 == 1) { t99 = 1; } else {
// inline call to triangle(98)
int t98;
if (100 - 1 - 1 == 1) { t98 = 1; } else {
// oops, "too big", no more inlining
t98 = triangle(100 - 1 - 1 - 1) + 98;
}
t99 = t98 + 99;
}
result = t99 + 100;
}
return result;
}
显然,优化者将有一个实地日,所以它比它看起来“更小”:
int t100() {
return triangle(97) + 297;
}
triangle
中的代码本身可以通过几个级别的内联“展开”,完全相同的方式,除了它没有常量的好处:
int triangle(int n) {
if (n == 1) return 1;
if (n == 2) return 3;
if (n == 3) return 6;
return triangle(n-3) + 3*n - 3;
}
我怀疑编译器是否真的这样做了,但我认为我没有注意到它[编辑:如果你告诉MSVC会这样做,感谢peterchen]。
在节省调用开销方面有明显的潜在好处,但是人们并不真正期望递归函数被内联,并且没有特别保证通常的内联启发式方法在递归函数中表现良好(其中有两个)不同的地方,呼叫站点和递归呼叫,可能内联,在每种情况下具有不同的好处)。此外,在编译时很难估计递归的深度,并且内联启发式方法可能会考虑调用深度来做出决策。所以可能是编译器没有打扰。
与C或C ++编译器相比,函数式语言编译器通常是 lot 更积极地处理递归。相关的权衡是,用函数式语言编写的这么多函数是递归的,如果编译器无法优化尾递归,那么性能可能毫无希望。所以Lisp程序员通常依赖于递归函数的良好优化,而C和C ++程序员通常不依赖。
答案 3 :(得分:4)
如果您的编译器不支持它,您可以尝试手动内联...
int factorial(int n) {
int result = 1;
if (n-- == 0) {
return result;
} else {
result *= 1;
if (n-- == 0) {
return result;
} else {
result *= 2;
if (n-- == 0) {
return result;
} else {
result *= 3;
if (n-- == 0) {
return result;
} else {
result *= 4;
if (n-- == 0) {
return result;
} else {
// ...
}
}
}
}
}
}
看到问题了吗?
答案 4 :(得分:3)
现在,坚持下去。尾递归函数可以很容易地展开和内联。显然有编译器会这样做,但我不知道具体细节。
答案 5 :(得分:3)
Tail recursion(一种递归的特例)可以通过 smart 编译器进行内联。
答案 6 :(得分:1)
当然。如果有意义的话,可以内联任何函数:
int f(int i)
{
if (i <= 0) return 1;
else return i * f(i - 1);
}
int main()
{
return f(10);
}
伪程序集(f在main中内联):
main:
mov r0, #10 ; Pass 10 to f
f:
cmp r0, #0 ; arg <= 0? ...
bge 1l
mov r0, #1 ; ... is so, return 1
ret
1:
mov r0, -(sp) ; if not, save arg.
dec r0 ; pass arg - 1 to f
call f ; just because it's inlined doesn't mean I can't call it.
mul r0, (sp)+ ; compute the result
ret ; done.
- )
答案 7 :(得分:0)
当您更改命令顺序执行顺序并将(调用或jmp)跳转到函数所在的某个地址时调用普通函数。内联意味着您在此函数的所有出现时都会放置此函数的命令,因此您没有可以跳转的位置,也可以使用其他类型的优化,例如推送/弹出函数参数的终止。 / p>
答案 8 :(得分:0)
如果你知道,递归链在正常情况下不会那么长,你可以内联到一个预定义的级别(我不知道,如果现有的编译器今天有足够的智能)。
内联递归函数非常类似于展开循环。你最终会得到很多重复的代码 - 但在某些情况下它可能是值得的:
答案 9 :(得分:0)
有些编译器会将尾递归转换为普通循环,因此可以正常内联它们。
非尾递归可以内联到给定深度,通常由编译器决定。
我从来没有遇到过这方面的实际应用,因为呼叫成本不足以抵消代码大小的增加。
[编辑](澄清一点:即使我喜欢玩这些东西,并且经常检查我的编译器为了“有趣的东西”生成的代码只是出于好奇,我没有遇到过一个用例任何此类展开都有显着帮助。这并不意味着它们不存在或无法构建。
它唯一有用的地方是在编译期间预先计算低迭代次数。但是,根据我的经验,这极大地增加了编译时间,通常可以忽略不计的运行时性能优势。
请注意,Visual Studio 2008(及更早版本)为您提供了相当多的控制权:
#pragma inline_recursion(on)
#pragma inline_depth(N)
__forceinline
小心后者,它很容易使编译器过载:)
答案 10 :(得分:0)
当然可以内联声明。 inline关键字只是编译器的提示。在许多情况下,编译器只是忽略它,并且根据编译器,这可能是这种情况之一。
答案 11 :(得分:-1)
内联意味着在每个地方调用标记为内联的函数,编译器会在那里放置所述函数代码的副本。这避免了函数调用机制,并且通常的参数堆栈推送弹出,节省了每秒数万亿次调用的时间。你看到静态变量和类似的东西的后果?一切都消失了......
所以,如果你有一个内联的递归调用,要么你的编译器是超级智能的,并且确定副本的数量是否是确定性的,它会说“不能使它内联”,因为它不知道何时停止。