【发布时间】:2011-01-13 11:39:16
【问题描述】:
【问题讨论】:
-
您如何建议内联递归函数?通过使用
inline指令? -
递归模板函数算吗?
【问题讨论】:
inline 指令?
一些编译器会将尾递归转换为普通循环,从而正常内联它们。
非尾递归可以内联到给定的深度,通常由编译器决定。
我从未遇到过这方面的实际应用,因为调用成本不再高到足以抵消代码大小的增加。
[编辑](澄清一下:尽管我喜欢玩弄这些东西,并且出于好奇经常检查我的编译器为“有趣的东西”生成了什么代码,但我还没有遇到过用例任何此类展开都有很大帮助。这并不意味着它们不存在或无法构建。
唯一有用的地方是在编译期间预先计算低迭代。但是,根据我的经验,这极大地增加了编译时间,而运行时性能优势通常可以忽略不计。
请注意,Visual Studio 2008(及更早版本)为您提供了相当多的控制权:
#pragma inline_recursion(on)
#pragma inline_depth(N)
__forceinline
小心后者,它很容易使编译器过载:)
【讨论】:
当然可以声明为内联。 inline 关键字只是对编译器的提示。在许多情况下,编译器只是忽略它,根据编译器,这可能是这种情况之一。
【讨论】:
当然。如果有意义,任何函数都可以内联:
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.
;-)
【讨论】:
当您知道递归链在正常情况下不会那么长时,您可以内联到预定义的级别(我不知道,今天是否有任何现有的编译器足够智能)。
内联递归函数很像展开循环。你最终会得到很多重复的代码——但在某些情况下它可能是值得的:
【讨论】:
当您更改命令顺序执行顺序并跳转(调用或jmp)到函数所在的某个地址时调用普通函数。内联意味着你在这个函数的所有出现的地方放置这个函数的命令,所以你没有一个可以跳转的地方,也可以使用其他类型的优化,比如取消推送/弹出函数参数。
【讨论】:
[编辑:刚刚注意到虽然您的标题说“内联”,但您的实际问题是“使函数内联”。这两者实际上没有任何关系,他们只是有令人困惑的相似名称。在现代编译器中,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++ 编译器更积极地处理递归。相关的权衡是用函数式语言编写的这么多函数都是递归的,如果编译器无法优化尾递归,那么性能可能毫无希望。所以 Lisp 程序员通常依赖递归函数的良好优化,而 C 和 C++ 程序员通常不会。
【讨论】:
inline_recursion 和 inline_depth。见msdn.microsoft.com/en-us/library/69hzy453.aspx。玩它真的很有趣,看看编译器做了什么。 +1 以获得深入的答案。
triangle(100) -> triangle(92) + 772 正如我“预测”的那样:-)
内联意味着在每次调用标记为内联的函数时,编译器都会将所述函数代码的副本放在那里。这避免了函数调用机制,并且它通常是参数堆栈推入弹出,在每秒大量调用的情况下节省时间。你看到静态变量和类似的东西的后果了吗?都过去了……
因此,如果您有一个内联递归调用,那么您的编译器要么是超级智能的,并且可以确定副本的数量是否具有确定性,它会说“无法使其内联”,因为它不知道何时停止。
【讨论】:
现在,等一下。尾递归函数可以很容易地展开和内联。显然有编译器可以做到这一点,但我不知道具体情况。
【讨论】:
如果您的编译器不支持,您可以尝试手动内联...
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 {
// ...
}
}
}
}
}
}
看到问题了吗?
【讨论】:
Tail recursion(递归的一种特殊情况)可以被 smart 编译器内联。
【讨论】:
可以通过尾递归优化的递归函数当然可以内联。如果一个函数做的最后一件事是调用自己,那么它可以被转换成一个普通的循环。
【讨论】:
不能内联任意递归函数,原因与蛇无法吞下自己的尾巴相同。
【讨论】: