【问题标题】:Trying to understand gcc option -fomit-frame-pointer试图理解 gcc 选项 -fomit-frame-pointer
【发布时间】:2013-01-17 23:13:19
【问题描述】:

我让 Google 告诉我 gcc 选项 -fomit-frame-pointer 的含义,这会将我重定向到以下语句。

-fomit-frame-pointer

不要将帧指针保存在不需要的函数的寄存器中。这避免了保存、设置和恢复帧指针的指令;它还在许多功能中提供了一个额外的寄存器。这也使得在某些机器上无法调试。

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

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

【问题讨论】:

  • 只需调试一个使用此选项编译的代码的崩溃转储就足以让您从您的 makefile 中删除此选项。顺便说一句,它不会删除任何指令,它只是为优化器提供了一个用于存储的寄存器。
  • @HansPassant 实际上,它对于发布版本非常有用。在 Makefile 中有两个目标 - ReleaseDebug 实际上非常有用,以这个选项为例。
  • @VladislavToncharov 我猜你从来不需要从运行你的Release-build 的客户那里调试崩溃转储?

标签: c performance gcc cpu-registers stack-frame


【解决方案1】:

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

这实际上是关于编译器如何管理堆栈的使用情况,以及堆栈上的位置(局部变量、传递给当前函数的参数以及为即将调用的函数准备的参数)。我认为描述需要或不需要帧指针的函数并不容易(从技术上讲,没有函数必须有一个帧指针 - 它更像是“如果编译器认为有必要降低复杂性其他代码”)。

我认为你不应该“尝试让函数没有帧指针”作为你编码策略的一部分——就像我说的,简单的函数不需要它们,所以使用-fomit-frame-pointer,你将为寄存器分配器再获得一个可用的寄存器,并保存 1-3 条关于进入/退出函数的指令。如果你的函数需要一个帧指针,那是因为编译器认为这是一个比不使用帧指针更好的选择。拥有没有帧指针的函数不是目标,目标是拥有既正确又快速运行的代码。

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

【讨论】:

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

这是关于英特尔平台上的 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

enter 指令,这在 Intel 80286 和 80386 处理器上非常有用。

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

mov esp, ebp
pop ebp 

leave 指令。

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

程序员可能对堆栈帧有疑问,不是广义的(它是堆栈中的一个实体,只服务一个函数调用并保留返回地址、参数和局部变量),而是狭义的——当术语stack frames 在编译器选项的上下文中被提及。从编译器的角度来看,堆栈帧只是例程的进入和退出代码,它将锚点推入堆栈——也可用于调试和异常处理。调试工具可能会扫描堆栈数据并使用这些锚点进行回溯,同时在堆栈中定位call sites,即按照分层调用函数的顺序显示函数的名称。

这就是为什么对于程序员来说,从编译器选项的角度理解什么是堆栈帧至关重要——因为编译器可以控制是否生成此代码。

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

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

从一般性回归特殊性:如果您使用-fomit-frame-pointer GCC 编译器选项,您可能会在例程的进入和退出代码以及额外的寄存器上获胜(除非它本身已经默认打开或通过其他选项隐式使用,在这种情况下,您已经从使用 EBP/RBP 寄存器的增益中受益,并且如果该选项已经隐式打开,则不会通过显式指定此选项获得额外增益)。但请注意,在 16 位和 32 位模式下,BP 寄存器无法像 AX(AL 和 AH)那样提供对 8 位部分的访问。

由于这个选项,除了允许编译器在优化中使用 EBP 作为通用寄存器之外,还可以防止为堆栈帧生成退出和进入代码,这会使调试复杂化——这就是为什么 GCC documentation 明确声明(不寻常用粗体强调)启用此选项使得在某些机器上无法进行调试

还请注意,与调试或优化相关的其他编译器选项可能会隐式打开或关闭 -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 平台上使用单个 `-O' 选项进行编译,-fomit-frame-pointer 是否会被打开。它可能会经过经验测试,但在这种情况下,GCC 开发人员不承诺将来不会更改此选项的行为,恕不另行通知。

不过,Peter Cordes 在 cmets 中指出,-fomit-frame-pointer 的默认设置在 x86-16 平台和 x86-32/64 平台之间存在差异。

这个选项 - -fomit-frame-pointer - 也是 relevant to the Intel C++ Compiler 15.0,不仅适用于 GCC:

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

这是英特尔所写的:

这些选项确定 EBP 是否用作优化中的通用寄存器。选项 -fomit-frame-pointer 和 /Oy 允许这样使用。选项 -fno-omit-frame-pointer 和 /Oy- 不允许。

一些调试器期望 EBP 被用作堆栈帧指针,除非这样,否则无法生成堆栈回溯。 -fno-omit-frame-pointer 和 /Oy- 选项指示编译器生成维护和使用 EBP 作为所有函数的堆栈帧指针的代码,以便调试器仍然可以在不执行以下操作的情况下生成堆栈回溯:

对于 -fno-omit-frame-pointer:使用 -O0 关闭优化 对于 /Oy-:关闭 /O1、/O2 或 /O3 优化 -fno-omit-frame-pointer 选项在您指定选项 -O0 或 -g 选项时设置。 -fomit-frame-pointer 选项在您指定选项 -O1、-O2 或 -O3 时设置。

当您指定 /O1、/O2 或 /O3 选项时,将设置 /Oy 选项。选项 /Oy- 在您指定 /Od 选项时设置。

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

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

请注意,以上引用仅与 Intel C++ 15 编译器相关,与 GCC 无关。

【讨论】:

  • 16 位代码和 BP 默认为 SS 而不是 DS,与 gcc 并不真正相关。 gcc -m16 存在,但这是一个奇怪的特殊情况,它基本上使 32 位代码在 16 位模式下运行,在所有地方都使用前缀。另请注意,-fomit-frame-pointer 在 x86 -m32 上默认启用多年,并且比 x86-64 (-m64) 上的启用时间更长。
  • @PeterCordes - 谢谢,我已根据您提出的问题更新了编辑内容。
  • 很好的答案!
【解决方案3】:

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

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

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

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

现在需要帧指针的主要时间是函数使用 alloca 或可变长度数组。在这种情况下,堆栈指针的值不能被静态跟踪。

【讨论】:

    猜你喜欢
    • 2018-11-10
    • 1970-01-01
    • 2014-11-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-01-15
    相关资源
    最近更新 更多