试图理解gcc选项-fomit-frame-pointer

ras*_*hok 74 c optimization performance gcc

我让Google告诉我该gcc选项的含义-fomit-frame-pointer,它将我重定向到以下声明.

-fomit帧指针

不要将帧指针保存在寄存器中以查找不需要的函数.这避免了保存,设置和恢复帧指针的指令; 它还在许多功能中提供额外的寄存器.它还使某些机器无法进行调试.

根据我对每个函数的了解,将在进程内存的堆栈中创建激活记录,以保留所有局部变量和更多信息.我希望这个帧指针意味着一个函数的激活记录的地址.

在这种情况下,什么是函数类型,它不需要将帧指针保持在寄存器中?如果我得到这个信息,我会尝试设计基于它的新函数(如果可能),因为如果帧指针没有保存在寄存器中,一些指令将在二进制中省略.在具有许多功能的应用程序中,这将显着提高性能.

Mat*_*son 53

大多数较小的函数不需要帧指针 - 较大的函数可能需要一个.

它实际上是关于编译器如何管理堆栈的使用情况,以及堆栈中的内容(局部变量,传递给当前函数的参数以及为即将调用的函数准备的参数).我不认为很容易表征需要或不需要帧指针的函数(从技术上讲,NO函数有一个帧指针 - 更多的情况是"如果编译器认为有必要降低复杂性其他代码").

我不认为你应该"尝试使函数没有帧指针"作为编码策略的一部分 - 就像我说的,简单的函数不需要它们,所以使用-fomit-frame-pointer,你将获得一个更多的寄存器对于寄存器分配器,在进入/退出函数时保存1-3条指令.如果你的函数需要一个帧指针,那是因为编译器决定这是一个比不使用帧指针更好的选择.拥有没有帧指针的函数并不是一个目标,它的目标是使代码能够正确和快速地工作.

请注意,"没有帧指针"应该会提供更好的性能,但它并不是一些能够带来巨大改进的神奇子弹 - 尤其是x86-64,它已经有16个寄存器.在32位x86上,因为它只有8个寄存器,其中一个是堆栈指针,并且占用另一个作为帧指针意味着占用了25%的寄存器空间.将其改为12.5%是一个很大的改进.当然,64位编译也会有很大帮助.

  • 值得一提的是,gcc默认为x86-64启用了`-fomit-frame-pointer`. (28认同)
  • 通常,编译器可以自己跟踪堆栈深度,而不需要帧指针.例外情况是该函数使用`alloca`将堆栈指针移动一个可变量.帧指针省略确实使调试变得更加困难.局部变量更难以定位,并且如果没有帧指针帮助重建堆栈跟踪就更难以重建.此外,访问参数可能会变得更加昂贵,因为它们远离堆栈的顶部并且可能需要更昂贵的寻址模式. (22认同)
  • @MatsPetersson VLA与`alloca`不同:当你离开声明它们的范围时它们就会被丢弃,而`alloca`空间只有在你离开函数时才被释放.我认为这使得VLA比`alloca`更容易遵循. (6认同)
  • @JensGustedt,问题不在于它们被扔掉时,问题是它们的大小(比如`alloca`'ed space)在_compile_时是未知的.通常编译器将使用帧指针来获取局部变量的地址,如果堆栈帧的大小没有改变,它可以将它们定位在与堆栈指针的固定偏移处. (5认同)
  • 是的,所以,假设我们没有使用`alloca` [谁呢? - 我99%肯定我从来没有编写过使用`alloca`]或`variable size local arrays` [这是`alloca`的现代形式]的代码,然后编译器仍然可以决定使用frame-pointer是一个更好的选择 - 因为编写器不是盲目地遵循给出的选项,而是给你最好的选择. (3认同)

Max*_*tin 11

这完全是关于英特尔平台上的BP/EBP/RBP注册.该寄存器默认为堆栈段(不需要特殊的前缀来访问堆栈段).

EBP是访问堆栈内数据结构,变量和动态分配工作空间的最佳选择.EBP通常用于相对于堆栈上的固定点而不是相对于当前TOS访问堆栈上的元素.它通常标识为当前过程建立的当前堆栈帧的基址.当EBP在偏移计算中用作基址寄存器时,偏移量将在当前堆栈段(即SS当前选择的段)中自动计算.由于不必明确指定SS,因此在这种情况下的指令编码更有效.EBP还可用于索引可通过其他段寄存器寻址的段.

(来源 - http://css.csail.mit.edu/6.858/2017/readings/i386/s02_03.htm)

由于在大多数32位平台上,数据段和堆栈段是相同的,因此EBP/RBP与堆栈的这种关联不再是问题.在64位平台上也是如此:AMD在2003年推出的x86-64架构在很大程度上放弃了对64位模式分段的支持:四个段寄存器:CS,SS,DS和ES被强制为0 x86 32位和64位平台的这些情况实质上意味着可以在访问存储器的处理器指令中使用EBP/RBP寄存器,而没有任何前缀.

因此,您编写的编译器选项允许BP/EBP/RBP用于其他方式,例如保存局部变量.

通过"这避免了保存,设置和恢复帧指针的指令"意味着避免在每个函数的条目上使用以下代码:

push ebp
mov ebp, esp
Run Code Online (Sandbox Code Playgroud)

enter指令,这对英特尔80286和80386处理器非常有用.

此外,在函数返回之前,使用以下代码:

mov esp, ebp
pop ebp 
Run Code Online (Sandbox Code Playgroud)

leave指示.

调试工具可以扫描堆栈数据并在定位时使用这些推送的EBP寄存器数据call sites,即以分层次调用它们的顺序显示函数名称和参数.

程序员可能对堆栈框架的问题不是很广泛(它是堆栈中的单个实体,只提供一个函数调用并保留返回地址,参数和局部变量),但从狭义上说 - 当stack frames提到术语时编译器选项的上下文.从编译器的角度来看,堆栈帧只是例程入口和出口代码,它将一个锚推送到堆栈 - 它也可以用于调试和异常处理.调试工具可以扫描堆栈数据并使用这些锚点进行回溯,同时定位call sites在堆栈中,即按照它们被分层调用的顺序显示函数的名称.

这就是为什么了解程序员在编译器选项方面的堆栈框架是非常重要的 - 因为编译器可以控制是否生成此代码.

在某些情况下,编译器可以省略堆栈帧(例程的入口和出口代码),并且可以通过堆栈指针(SP/ESP/RSP)直接访问变量,而不是方便的基址指针(BP/ESP/RSP).编译器省略某些功能的堆栈帧的条件可能不同,例如:(1)该功能是叶子功能(即不调用其他功能的终端实体); (2)不使用例外; (3)没有用栈上的传出参数调用例程; (4)该功能没有参数.

省略堆栈帧(例程的进入和退出代码)可以使代码更小更快,但也可能对调试器反向跟踪堆栈中的数据并将其显示给程序员的能力产生负面影响.这些是编译器选项,用于确定函数应在哪些条件下满足,以便编译器使用堆栈帧条目和退出代码对其进行授予.例如,在以下情况下,编译器可以选择将此类入口和退出代码添加到函数中:(a)始终,(b)从不,(c)在需要时(指定条件).

从一般性返回到特殊性:如果您将使用-fomit-frame-pointerGCC编译器选项,您可以在例程的入口和出口代码上获胜,并且可以获得额外的寄存器(除非它已经默认为自身或其他人隐式启用选项,在这种情况下,您已经从使用EBP/RBP寄存器获益中获益,如果已经隐式使用此选项,则不会获得额外的增益.但请注意,在16位和32位模式下,BP寄存器无法像AX具有(AL和AH)那样访问它的8位部分.

由于此选项除了允许编译器在优化中使用EBP作为通用寄存器外,还可以防止为堆栈帧生成退出和输入代码,从而使调试变得复杂 - 这就是GCC文档明确指出的原因(异常强调大胆如果启用此选项,则在某些计算机上无法进行调试

另请注意,与调试或优化相关的其他编译器选项可能会隐式打开-fomit-frame-pointer或关闭选项.

我在gcc.gnu.org上没有找到关于其他选项如何影响-fomit-frame-pointer x86平台的官方信息,https://gcc.gnu.org/onlinedocs/gcc-3.4.4/gcc/Optimize-Options.html仅说明以下内容:

-O还会在机器上启用-fomit-frame-pointer,这样做不会干扰调试.

因此,如果您只是在x86平台上使用单个选项进行编译,那么从文档本身是否-fomit-frame-pointer会打开是不明确的-O.它可以根据经验进行测试,但在这种情况下,GCC开发人员没有承诺以后不更改此选项的行为,恕不另行通知.

但是,Peter Cordes在评论中指出,-fomit-frame-pointerx86-16平台和x86-32/64平台之间的默认设置存在差异.

此选项 - -fomit-frame-pointer- 也与英特尔C++编译器15.0相关,不仅与GCC相关:

对于英特尔编译器,此选项具有别名/Oy.

这是英特尔写的关于它的内容:

这些选项确定EBP是否在优化中用作通用寄存器.选项-fomit-frame-pointer和/ Oy允许这种用法.选项-fno-omit-frame-pointer和/ Oy-禁止它.

一些调试器期望EBP被用作堆栈帧指针,并且除非是这样,否则不能产生堆栈回溯.-fno-omit-frame-pointer和/ Oy- options指示编译器生成代码,该代码维护并使用EBP作为所有函数的堆栈帧指针,以便调试器仍然可以生成堆栈回溯而不执行以下操作:

对于-fno-omit-frame-pointer:使用-O0关闭优化For/Oy-:关闭/ O1,/ O2或/ O3优化指定选项时设置-fno-omit-frame-pointer选项 - O0或-g选项.指定选项-O1,-O2或-O3时,将设置-fomit-frame-pointer选项.

指定/ O1,/ O2或/ O3选项时,将设置/ Oy选项.选择/ Oy-指定/ Od选项时设置.

使用-fno-omit-frame-pointer或/ Oy-选项可将通用寄存器的数量减少1,从而导致代码效率稍低.

注意对于Linux*系统:目前GCC 3.2异常处理存在问题.因此,当为C++安装GCC 3.2并打开异常处理时(默认情况下),英特尔编译器会忽略此选项.

请注意,上述引用仅适用于英特尔C++ 15编译器,而不适用于GCC.

  • 16 位代码和 BP 默认为 SS 而不是 DS,与 gcc 并不真正相关。`gcc -m16` 存在,但这是一个奇怪的特殊情况,它基本上使 32 位代码在 16 位模式下运行,到处都使用前缀。另请注意,默认情况下,在 x86 上启用了 `-fomit-frame-pointer` 多年,`-m32` 比在 x86-64 (`-m64`) 上启用的时间更长。 (2认同)
  • 很好的答案! (2认同)

plu*_*ash 5

我以前没有遇到过“激活记录”这个术语,但我认为它指的是通常所说的“堆栈帧”。这是当前函数使用的堆栈区域。

帧指针是一个寄存器,保存当前函数的堆栈帧的地址。如果使用帧指针,则在进入函数时,旧帧指针将保存到堆栈中,并将帧指针设置为堆栈指针。离开该函数时,旧的帧指针将恢复。

大多数普通函数本身的操作不需要帧指针。编译器可以通过函数跟踪所有代码路径上的堆栈指针偏移量,并相应地生成局部变量访问。

在某些上下文中,帧指针对于调试和异常处理可能很重要。尽管现代调试和异常处理格式被设计为在大多数情况下支持没有帧指针的函数,但这种情况变得越来越罕见。

如今,主要需要帧指针的情况是函数使用分配或可变长度数组。在这种情况下,无法静态跟踪堆栈指针的值。