如何防止递归函数调用(tail-recursion)堆积在堆栈中?

时间:2019-05-13 03:33:50

标签: c++ recursion stack

当函数在最后一行或使用return命令调用自身时,似乎没有必要将调用者保持在堆栈中。

我用“ gcc”测试了这一理论,发现调用者函数仍然在堆栈中:

#include <iostream>

void a(int i)
{
    std::cout << i << std::endl;
    if (i > 0)
        a(i - 1);
    // Also tested return a(i - 1);
}

int main()
{
    a(10);
}

调用堆栈:

...
a(int i) (/mnt/temp/hackerrank/src/main.cpp:30)
a(int i) (/mnt/temp/hackerrank/src/main.cpp:30)
a(int i) (/mnt/temp/hackerrank/src/main.cpp:30)
main() (/mnt/temp/hackerrank/src/main.cpp:35)

为什么优化不强制弹出父级?

根据评论:该主题以“尾递归”而闻名。

1 个答案:

答案 0 :(得分:1)

我已经将它扔进godbolt

在具有-O3优化的gcc(8.3)和clang(8.0.0)上,该函数编译为无操作函数。令人印象深刻的是,对于具有/O2优化的MSVC v19.20甚至如此。

GCC 8.3 / Clang 8.0.0:

a(int):
        ret

MSVC v19.20(x64):

i$ = 8
void a(int) PROC                               ; a, COMDAT
        ret     0
void a(int) ENDP                               ; a

我也毫不犹豫地使示例变得平凡。我从这里开始使用的代码是:

#include <iostream>

void a(int i)
{
    std::cout << "hello\n";
    if (i > 0)
        a(i - 1);
}

启用了-O3优化的gcc trunc的编译器输出如下:

.LC0:
        .string "hello\n"
a(int):
        push    rbx
        mov     ebx, edi
        jmp     .L3
.L6:
        sub     ebx, 1
.L3:
        mov     edx, 6
        mov     esi, OFFSET FLAT:.LC0
        mov     edi, OFFSET FLAT:_ZSt4cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
        test    ebx, ebx
        jg      .L6
        pop     rbx
        ret
_GLOBAL__sub_I_a(int):
        sub     rsp, 8
        mov     edi, OFFSET FLAT:_ZStL8__ioinit
        call    std::ios_base::Init::Init() [complete object constructor]
        mov     edx, OFFSET FLAT:__dso_handle
        mov     esi, OFFSET FLAT:_ZStL8__ioinit
        mov     edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
        add     rsp, 8
        jmp     __cxa_atexit

经过仔细检查,唯一的call指令是io方法在每次迭代中写入消息。然后执行test(if语句)。如果i > 0,则控制跳至上,递减i,然后再次执行所有操作。 if语句的另一个分支(假情况)只是返回(在堆栈清理之后)。

因此,即使在这个不平凡的示例中,也不会堆积堆栈帧。它是通过jmp指令执行的,因为(如您所说)先前的执行信息是无关紧要的。