何时应省略帧指针?

34

省略帧指针(frame pointer)会有实质性的优化吗? 如果我正确理解了这个页面-fomit-frame-pointer用于避免保存、设置和恢复帧指针。

这只在每次函数调用时完成,如果是这样的话,每个函数避免一些指令真的很值得吗? 不是一个微不足道的优化吗? 除了调试限制外,使用此选项的实际影响是什么?

我使用和不使用这个选项编译了以下C代码:

int main(void)
{
        int i;

        i = myf(1, 2);
}

int myf(int a, int b)
{
        return a + b;
}
# gcc -S -fomit-frame-pointer code.c -o withoutfp.s
# gcc -S code.c -o withfp.s

.

diff -u 对比这两个文件后,显示了以下汇编代码:


--- withfp.s    2009-12-22 00:03:59.000000000 +0000
+++ withoutfp.s 2009-12-22 00:04:17.000000000 +0000
@@ -7,17 +7,14 @@
        leal    4(%esp), %ecx
        andl    $-16, %esp
        pushl   -4(%ecx)
-       pushl   %ebp
-       movl    %esp, %ebp
        pushl   %ecx
-       subl    $36, %esp
+       subl    $24, %esp
        movl    $2, 4(%esp)
        movl    $1, (%esp)
        call    myf
-       movl    %eax, -8(%ebp)
-       addl    $36, %esp
+       movl    %eax, 20(%esp)
+       addl    $24, %esp
        popl    %ecx
-       popl    %ebp
        leal    -4(%ecx), %esp
        ret
        .size   main, .-main
@@ -25,11 +22,8 @@
 .globl myf
        .type   myf, @function
 myf:
-       pushl   %ebp
-       movl    %esp, %ebp
-       movl    12(%ebp), %eax
-       addl    8(%ebp), %eax
-       popl    %ebp
+       movl    8(%esp), %eax
+       addl    4(%esp), %eax
        ret
        .size   myf, .-myf
        .ident  "GCC: (GNU) 4.2.1 20070719 

能否有人阐述一下上述代码中,-fomit-frame-pointer 真正起到作用的关键点是什么?

编辑:gcc -S替换了 objdump 的输出


3
尝试使用-S编译再次进行差异比较。比较汇编语言:它会更易读。 - Richard Pennington
相关内容,请参见ARM:链接寄存器和帧指针 - jww
4个回答

35

-fomit-frame-pointer允许额外的寄存器用于通用目的。我认为这只在32位x86上很重要,因为它缺乏寄存器。

我们可以看到EBP不再在每个函数调用中保存和调整,并且可能在常规代码中使用EBP更多,并且在使用EBP作为通用寄存器时减少堆栈操作。

您的代码过于简单,无法从此类优化中获益——您没有使用足够的寄存器。同时,您没有启用优化器,这可能是必要的才能看到其中的某些效果。

* ISA寄存器,而非微架构寄存器。


如果我需要显式设置其他优化选项,那么单独存在这个选项的意义是什么?虽然你说我的代码很简单,但是这个观点似乎是正确的! - PetrosB
这个选项是独立的,因为它在调试方面有很大的缺陷。 - Anon.
它是独立的,因为它对其他事情有功能上的影响,比如在调试器中运行代码或与其他代码链接。我假设即使关闭优化器,您也会看到寄存器溢出的减少,但由于我不确定,所以我正在保守我的赌注。 - Eric Seppanen
@EricSeppanen,您能详细说明ISA和微架构寄存器之间的区别吗?您是否指的是后者中的x87/MMX/SSE等?还是“内部”寄存器(如eip)?谢谢! - andreee
我意识到微架构寄存器很可能是指程序员看不到的寄存器,而是通过寄存器重命名等方式访问的。有人能确认这是真的吗?谢谢! - andreee
1
@andreee:是的,这个答案非常明确地意味着架构寄存器与物理寄存器之间的区别(架构寄存器被重命名)。寄存器重命名可以避免在您为不同值(当您用完该位置上的旧值时)重复使用相同的架构寄存器时出现错误依赖,但是如果您需要在同一个循环中使用9个不同的变量,“活”着,它对您没有帮助。 - Peter Cordes

11

省略它唯一的缺点是调试更加困难。

而好处在于增加了一个额外的通用寄存器,这可以大大提高性能。显然,只有在需要时才使用这个额外的寄存器(可能在你非常简单的函数中不需要);在某些函数中,它比其他函数更具优势。


1
不仅使调试变得更加困难,GNU文档在线表示它使调试变得不可能。 - PetrosB
18
他们是错的。例如,使用printf()进行调试(这仍然是调试)是完全可行的。 - Andreas Bonini
11
无论使用任何编译器选项,您仍然可以在指令(汇编语言)级别进行调试。 当然,这不像源代码级别的调试那么容易,但“不可能”绝对是错误的说法。 - Ben Voigt
这个答案已经过时了(即使在发布时也是如此)。现代的调试信息格式(如DWARF)使得调试器能够在优化代码中查找变量值,即使它们存在于寄存器中(如果存在的话)。对于未经优化的代码,在GNU/Linux上使用“gcc -O0 -fomit-frame-pointer -g”对调试几乎没有影响。并不是说你应该在调试构建中使用“-fomit-frame-pointer”,但这证明了它并不会使调试更加困难。 - Peter Cordes
仍有一些情况需要帧指针以获得良好的调试体验。Linux内核没有DWARF解析器(并且不太可能获得一个),因此在内核中进行堆栈跟踪的工具(例如bpftrace的ustack())如果没有帧指针,则无法正常工作。 - nemetroid

7
您可以通过使用-S参数输出汇编代码来获得更有意义的GCC汇编代码:
$ gcc code.c -S -o withfp.s
$ gcc code.c -S -o withoutfp.s -fomit-frame-pointer
$ diff -u withfp.s withoutfp.s

GCC并不关心地址,因此我们可以直接比较生成的实际指令。对于你的叶子函数,这将给出:

 myf:
-       pushl   %ebp
-       movl    %esp, %ebp
-       movl    12(%ebp), %eax
-       addl    8(%ebp), %eax
-       popl    %ebp
+       movl    8(%esp), %eax
+       addl    4(%esp), %eax
    ret

GCC不会生成将帧指针推入堆栈的代码,这会改变在堆栈上传递给函数的参数的相对地址。


5

对程序进行分析以查看是否存在显著差异。

接下来,对开发过程进行分析。调试是否更容易或更困难?开发时间更长还是更短?

没有进行分析的优化是浪费时间和金钱。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接