用while循环代替递归

时间:2021-01-30 05:56:08

标签: c loops recursion assembly tail-recursion

出于性能原因,递归是否曾被 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 的情况?

2 个答案:

答案 0 :(得分:3)

是的,尾调用消除是一种常见的优化。如果您还没有看过,请查看 https://en.wikipedia.org/wiki/Tail_call,它详细讨论了这个主题。

<块引用>

GCC、LLVM/Clang 和英特尔编译器套件在更高的优化级别或在传递 -foptimize-sibling-calls 选项时为 C 和其他语言执行尾调用优化。尽管给定的语言语法可能不明确支持它,但只要编译器可以确定调用者和被调用者的返回类型是等效的,并且传递给两个函数的参数类型相同,或者需要调用堆栈上的总存储空间相同。

Wiki 页面还有一个汇编示例,说明优化器如何将递归例程修改为循环。

答案 1 :(得分:3)

是的。

证明: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!"

其他编译器生成类似的结果。

相关问题