为什么gcc对一个版本执行尾调用优化而对另一个版本执行尾调用优化?

ead*_*ead 6 c++ optimization gcc tail-call-optimization

尝试尾调用优化(tco),我偶然发现了以下奇怪的例子:

unsigned long long int fac1(unsigned long long int n){
  if (n==0)
    return 1;
  return n*fac1(n-1);
}
Run Code Online (Sandbox Code Playgroud)

实际上,我印象深刻,gcc能够在这里执行tco(带-O2标志),因为它不是那么直接:

fac1(unsigned long long):
        testq   %rdi, %rdi
        movl    $1, %eax
        je      .L4
.L3:
        imulq   %rdi, %rax
        subq    $1, %rdi
        jne     .L3
        rep ret
.L4:
        rep ret
Run Code Online (Sandbox Code Playgroud)

但是,从返回类型更改unsigned long long intunsigned intgcc后无法执行tlo:

unsigned int fac2(unsigned long long int n){
  if (n==0)
    return 1;
  return n*fac2(n-1);
}
Run Code Online (Sandbox Code Playgroud)

我们可以清楚地看到生成的程序集中的递归调用:

fac2(unsigned long long):
        testq   %rdi, %rdi
        jne     .L16
        movl    $1, %eax
        ret
.L16:
        pushq   %rbx
        movq    %rdi, %rbx
        leaq    -1(%rdi), %rdi
        call    fac2(unsigned long long)
        imull   %ebx, %eax
        popq    %rbx
        ret
Run Code Online (Sandbox Code Playgroud)

起初,我认为这是一个错过的优化,但现在我不确定,因为clang也无法执行此优化.所以也许我不知道哪种语言可以阻止这种优化.

为什么gcc不对该函数执行尾调用优化,fac2但仅用于fac1


我很清楚,在第二个版本中,必须降低结果.显然这是唯一的区别.但为什么这是一个问题并阻止tlo?

例如,如果我帮助编译器并将我的函数重写为经典的尾递归(它应该产生与版本相同的结果fac2):

unsigned int tlo_fac(unsigned long long int n, unsigned long long int cur){
  if (n==0)
    return cur;
  return tlo_fac(n-1, n*cur);
}

unsigned int fac(unsigned long long int n){
  return tlo_fac(n,1);
}
Run Code Online (Sandbox Code Playgroud)

我得到了一个与之相同fac1的tlo优化版本 (高32位允许包含垃圾,因此imulq可以在内联后使用):

fac(unsigned long long):
        testq   %rdi, %rdi
        movl    $1, %eax
        je      .L10
.L11:
        imulq   %rdi, %rax
        subq    $1, %rdi
        jne     .L11
.L10:
        rep ret
Run Code Online (Sandbox Code Playgroud)

Gau*_*gal 2

在 中fact2(),递归完成后,需要从unsigned long long int到进行强制转换unsigned int

unsigned int fac2(unsigned int n)产生以下组件,

fac2(unsigned int):
        testl   %edi, %edi
        movl    $1, %eax
        je      .L10
.L9:
        imull   %edi, %eax
        subl    $1, %edi
        jne     .L9
        rep ret
.L10:
        rep ret
Run Code Online (Sandbox Code Playgroud)

  • 这可能会让 GCC 感到困惑,但它并不会“从根本上”阻止将其重写为迭代 (2认同)
  • 实际上,顺序是:递归调用、乘法、转换。因此,即使没有强制转换,递归调用也不是函数中完成的最后一件事(这是首先给我留下深刻印象的部分) (2认同)