如何检查gcc是否正在执行尾递归优化?

时间:2009-01-29 02:42:32

标签: gcc g++ tail-recursion

如何判断gcc(更具体地说,g ++)是否在特定函数中优化尾递归? (因为它出现了几次:我不想测试gcc是否可以优化尾递归。我想知道它是否优化了我的尾递归函数。)

如果您的答案是“查看生成的汇编程序”,我想知道我正在寻找什么,以及我是否可以编写一个简单的程序来检查汇编程序以查看是否存在优化。 / p>

PS。我知道这似乎是5个月前问题Which, if any, C++ compilers do tail-recursion optimization?的一部分。但是,我不认为该问题的部分得到了令人满意的答复。 (答案是“检查编译器是否进行了优化(我知道)的最简单方法是执行调用,否则会导致堆栈溢出 - 或者查看汇编输出。”)

8 个答案:

答案 0 :(得分:62)

让我们使用the example code from the other question。编译它,但告诉gcc不要汇编:

gcc -std=c99 -S -O2 test.c

现在让我们看看结果 test.s 文件中的_atoi函数(Mac OS 10.5上的gcc 4.0.1):

        .text
        .align 4,0x90
_atoi:
        pushl   %ebp
        testl   %eax, %eax
        movl    %esp, %ebp
        movl    %eax, %ecx
        je      L3
        .align 4,0x90
L5:
        movzbl  (%ecx), %eax
        testb   %al, %al
        je      L3
        leal    (%edx,%edx,4), %edx
        movsbl  %al,%eax
        incl    %ecx
        leal    -48(%eax,%edx,2), %edx
        jne     L5
        .align 4,0x90
L3:
        leave
        movl    %edx, %eax
        ret

编译器已对此函数执行了尾调用优化。我们可以判断,因为该代码中没有call指令,而原始C代码显然有一个函数调用。此外,我们可以看到jne L5指令,它在函数中向后跳转,指示当C代码中没有明显的循环时的循环。如果您在关闭优化的情况下重新编译,则会看到一行显示call _atoi,并且您也看不到任何向后跳转。

是否可以自动化这是另一回事。汇编程序代码的细节取决于您正在编译的代码。

我认为你可以通过编程方式发现它。使该函数打印出堆栈指针的当前值(在x86上注册ESP)。如果函数为第一次调用打印的值与递归调用的值相同,则编译器已执行尾调用优化。这个想法需要修改你希望观察到的功能,这可能会影响编译器选择优化函数的方式。如果测试成功(两次打印相同的ESP值),那么我认为假设优化也将在没有您的仪器的情况下执行是合理的,但如果测试失败,我们将无法知道失败是否是由于添加了仪器代码。

答案 1 :(得分:11)

编辑我的原始帖子也阻止了GCC实际进行尾部呼叫抵消。我在傻瓜GCC下面添加了一些额外的技巧,无论如何都要进行尾部调用消除。

扩展Steven的答案,你可以通过编程方式检查你是否有相同的堆栈框架:

#include <stdio.h>

// We need to get a reference to the stack without spooking GCC into turning
// off tail-call elimination
int oracle2(void) { 
    char oracle; int oracle2 = (int)&oracle; return oracle2; 
}

void myCoolFunction(params, ..., int tailRecursionCheck) {
    int oracle = oracle2();
    if( tailRecursionCheck && tailRecursionCheck != oracle ) {
        printf("GCC did not optimize this call.\n");
    }
    // ... more code ...
    // The return is significant... GCC won't eliminate the call otherwise
    return myCoolFunction( ..., oracle);
}

int main(int argc, char *argv[]) {
    myCoolFunction(..., 0);
    return 0;
}

以非递归方式调用函数时,将check参数传入0。否则传入oracle。如果一个应该被淘汰的尾递归调用没有,那么你将在运行时得到通知。

测试时,看起来我的GCC版本没有优化第一个尾调用,但剩余的尾调用已经过优化。有趣。

答案 2 :(得分:7)

查看生成的汇编代码,看看它是否在x86上使用calljmp指令进行递归调用(对于其他架构,请查看相应的指令)。您可以使用nmobjdump来获取与您的函数对应的程序集。考虑以下功能:

int fact(int n)
{
  return n <= 1 ? 1 : n * fact(n-1);
}

编译为

gcc fact.c -c -o fact.o -O2

然后,测试它是否使用尾递归:

# get starting address and size of function fact from nm
ADDR=$(nm --print-size --radix=d fact.o | grep ' fact$' | cut -d ' ' -f 1,2)
# strip leading 0's to avoid being interpreted by objdump as octal addresses
STARTADDR=$(echo $ADDR | cut -d ' ' -f 1 | sed 's/^0*\(.\)/\1/')
SIZE=$(echo $ADDR | cut -d ' ' -f 2 | sed 's/^0*//')
STOPADDR=$(( $STARTADDR + $SIZE ))

# now disassemble the function and look for an instruction of the form
# call addr <fact+offset>
if objdump --disassemble fact.o --start-address=$STARTADDR --stop-address=$STOPADDR | \
    grep -qE 'call +[0-9a-f]+ <fact\+'
then
    echo "fact is NOT tail recursive"
else
    echo "fact is tail recursive"
fi

当运行上面的函数时,这个脚本打印“fact is tail recursive”。当使用-O3而不是-O2进行编译时,这很奇怪地打印出“事实不是尾递归”。

请注意,这可能会产生漏报,正如他的评论中指出的那样。如果函数根本不包含对自身的递归调用,则此脚本将只生成正确的答案,并且它也不会检测同级递归(例如A()调用B()调用A()的{​​{1}} )。我现在想不出一个更强大的方法,不需要人工查看生成的程序集,但至少可以使用此脚本轻松获取对应于目标文件中特定函数的程序集。

答案 3 :(得分:6)

扩展PolyThinker的答案,这是一个具体的例子。

int foo(int a, int b) {
    if (a && b)
        return foo(a - 1, b - 1);
    return a + b;
}

i686-pc-linux-gnu-gcc-4.3.2 -Os -fno-optimize-sibling-calls输出:

00000000 <foo>:
   0:   55                      push   %ebp
   1:   89 e5                   mov    %esp,%ebp
   3:   8b 55 08                mov    0x8(%ebp),%edx
   6:   8b 45 0c                mov    0xc(%ebp),%eax
   9:   85 d2                   test   %edx,%edx
   b:   74 16                   je     23 <foo+0x23>
   d:   85 c0                   test   %eax,%eax
   f:   74 12                   je     23 <foo+0x23>
  11:   51                      push   %ecx
  12:   48                      dec    %eax
  13:   51                      push   %ecx
  14:   50                      push   %eax
  15:   8d 42 ff                lea    -0x1(%edx),%eax
  18:   50                      push   %eax
  19:   e8 fc ff ff ff          call   1a <foo+0x1a>
  1e:   83 c4 10                add    $0x10,%esp
  21:   eb 02                   jmp    25 <foo+0x25>
  23:   01 d0                   add    %edx,%eax
  25:   c9                      leave
  26:   c3                      ret

i686-pc-linux-gnu-gcc-4.3.2 -Os输出:

00000000 <foo>:
   0:   55                      push   %ebp
   1:   89 e5                   mov    %esp,%ebp
   3:   8b 55 08                mov    0x8(%ebp),%edx
   6:   8b 45 0c                mov    0xc(%ebp),%eax
   9:   85 d2                   test   %edx,%edx
   b:   74 08                   je     15 <foo+0x15>
   d:   85 c0                   test   %eax,%eax
   f:   74 04                   je     15 <foo+0x15>
  11:   48                      dec    %eax
  12:   4a                      dec    %edx
  13:   eb f4                   jmp    9 <foo+0x9>
  15:   5d                      pop    %ebp
  16:   01 d0                   add    %edx,%eax
  18:   c3                      ret

在第一种情况下,<foo+0x11>-<foo+0x1d>推送函数调用的参数,而在第二种情况下,<foo+0x11>-<foo+0x14>将变量和jmp修改为同一函数,在序言之后的某处。这就是你想要寻找的东西。

我认为你不能以编程方式做到这一点;有太多可能的变化。函数的“肉”可能更接近或远离开始,并且您无法在不查看的情况下将jmp与循环或条件区分开来。它可能是条件跳转而不是jmpgcc可能会在某些情况下留下call,但会将兄弟调用优化应用于其他情况。

仅供参考,gcc的“兄弟调用”比尾递归调用略胜一筹 - 实际上,重新使用相同堆栈帧的任何函数调用都可能是兄弟般的调用。

[编辑]

作为一个例子,当只是寻找一个自我递归call时会误导你,

int bar(int n) {
    if (n == 0)
        return bar(bar(1));
    if (n % 2)
        return n;
    return bar(n / 2);
}

GCC会将兄弟呼叫优化应用于三个bar呼叫中的两个。我仍称它为尾调用优化,因为即使您在生成的程序集中找到call <bar+..>,单个未优化的调用也不会超过单个级别。

答案 4 :(得分:3)

我懒得去看一个反汇编。试试这个:

void so(long l)
{
    ++l;
    so(l);
}
int main(int argc, char ** argv)
{
    so(0);
    return 0;
}

编译并运行此程序。如果它永远运行,尾递归被优化掉了。如果它吹了堆栈,那不是。

编辑:抱歉,读得太快,OP想知道他的特定功能是否已将其尾递归优化掉了。 OK ......

......原理仍然相同 - 如果尾部递归被优化掉,那么堆栈帧将保持不变。您应该能够使用backtrace function从函数中捕获堆栈帧,并确定它们是否在增长。如果正在优化尾递归,那么缓冲区中只有一个返回指针

答案 5 :(得分:2)

我检查的另一种方法是:

  1. 使用'gcc -O2'
  2. 编译代码
  3. 启动'gdb'
  4. 在您希望进行尾递归优化/消除的函数中放置一个断点
  5. 运行您的代码
  6. 如果已经消除了尾部调用,则断点将仅被击中一次或从不被击中。有关详情,请参阅this

答案 6 :(得分:1)

一个简单的方法:构建一个简单的尾递归程序,编译它,然后拆分它以查看它是否已经过优化。

刚才意识到你的问题已经存在了。如果您知道如何阅读汇编,那么很容易理解。递归函数将在函数体内调用自身(带有“call label”),循环将只是“jmp label”。

答案 7 :(得分:0)

如果没有优化并且看看它是否发生,你可以制作会导致堆栈溢出的输入数据,因为该函数调用的递归过于深入。当然,这不是微不足道的,有时候足够大的输入会让这个功能长时间无法运行。