问题描述
出于性能原因,递归是否曾被 while
循环替代?我知道代码看起来更难看,但让我们举个例子:
void print_countdown(long long n) {
if (n!=0) {
printf("Placeholder for a function\n");
print_countdown(n-1);
} else
printf("Done!\n");
}
如果数字是 100 万,那么这将有以下开销:
- 100 万份
n
var(从 rdi 中保存,放回 rdi 中进行递归调用,如果递归工作包含函数调用,否则可以留在 rdi 中。) call func
-
push rbp ... pop
函数序言或用于堆栈对齐的其他内容,具体取决于优化级别和编译器选择。
换句话说,虽然代码是可读的,但对于超过 1000 次循环,这似乎更好地重写为:
void print_countdown(long long n) {
while (n < MAX_LOOPS) {
if (n!=0) {
printf("Placeholder for a function\n");
n = n-1;
} else
printf("Done!");
}
}
assembly code (Godbolt) 在 while
格式中看起来也简单得多 -- ~20 行 vs ~40 行。
进行这种循环重写是否很常见,并且在递归函数调用中是否存在无法将其重写为 loop
的情况?
解决方法
是的,尾调用消除是一种常见的优化。如果您还没有看过,请查看 https://en.wikipedia.org/wiki/Tail_call,它详细讨论了这个主题。
GCC、LLVM/Clang 和英特尔编译器套件在更高的优化级别或在传递 -foptimize-sibling-calls
选项时为 C 和其他语言执行尾调用优化。尽管给定的语言语法可能不明确支持它,但只要编译器可以确定调用者和被调用者的返回类型是等效的,并且传递给两个函数的参数类型相同,或者需要调用堆栈上的总存储空间相同。
Wiki 页面还有一个汇编示例,说明优化器如何将递归例程修改为循环。
,是的。
证明:https://godbolt.org/z/EqbnfY
此代码
#include <stdio.h>
void print_countdown(long long n) {
if (n!=0) {
// do_something
print_countdown(n-1);
} else
printf("Done!\n");
}
使用 x86-64 Clang 编译器和 -O2
优化生成此程序集(编译器甚至还生成注释!)
print_countdown(long long): # @print_countdown(long long)
mov edi,offset .Lstr
jmp puts # TAILCALL
.Lstr:
.asciz "Done!"
其他编译器生成类似的结果。