省略帧指针真的会对性能产生积极影响并对调试能力产生负面影响吗?

时间:2012-10-22 07:00:08

标签: visual-studio-2010 performance stackframe

正如很久以前所建议的,我总是在没有帧指针的情况下构建我的发布可执行文件(如果用/ Ox编译,这是默认的。)

但是,现在我在论文http://research.microsoft.com/apps/pubs/default.aspx?id=81176中读到,帧指针对性能没有太大影响。所以完全优化它(使用/ Ox)或使用帧指针(使用/ Ox / Oy-)完全优化它并不会对性能产生重大影响。

微软似乎表示添加帧指针(/ Oy-)会使调试变得更容易,但情况确实如此吗?

我做了一些实验并注意到:

  • 在一个简单的测试可执行文件中(使用/ Ox / Ob0编译),省略帧指针确实提高了性能(大约10%)。但是这个测试可执行文件只执行一些函数调用,没有别的。
  • 在我自己的应用程序中,添加/删除帧指针似乎没有太大的影响。添加帧指针似乎使应用程序快5%,但这可能在误差范围内。

关于帧指针的一般建议是什么?

  • 是否应该在发布可执行文件中省略(/ Ox),因为它们确实会对性能产生积极影响?
  • 是否应该在发布可执行文件中添加(/ Ox / Oy-),因为它们可以提高调试能力(使用崩溃转储文件进行调试时)?

使用Visual Studio 2010。

1 个答案:

答案 0 :(得分:22)

简答:通过省略帧指针

您需要使用堆栈指针来访问局部变量和参数。编译器并不介意,但如果你在编码中,这会让你的生活变得更加困难。如果你不使用宏,那就更难了。

每个函数调用可以节省四个字节(32位架构)的堆栈空间。除非你使用深度递归,否则这不是一个胜利。

您将内存写入保存到缓存内存(堆栈),并且您(理论上)在函数进入/退出时保存几个时钟周期,但您可以增加代码大小。除非你的功能经常做得很少(在这种情况下应该内联),这不应该是显而易见的。

您释放了通用注册表。如果编译器可以利用寄存器,它将产生既小又可能更快的代码。但是,如果花费大部分CPU时间与主存储器(甚至是硬盘驱动器)进行通信,那么省略帧指针就不会让你远离它。

调试器将失去一种生成堆栈跟踪的简便方法。调试器可能仍然能够从不同的源(例如PDB file)生成堆栈跟踪。


答案很长:

典型的函数进入和退出是:

PUSH SP   ;push the frame pointer
MOV FP,SP ;store the stack pointer in the frame pointer
SUB SP,xx ;allocate space for local variables et al.
...
LEAVE     ;restore the stack pointer and pop the old frame pointer
RET       ;return from the function

没有堆栈指针的入口和出口可能如下所示:

SUB SP,xx ;allocate space for local variables et al.
...
ADD SP,xx ;de-allocate space for local variables et al.
RET       ;return from the function.

你将保存两条指令,但你也复制了一个文字值,所以代码不会变得更短(完全相反),但是你可能已经保存了几个时钟周期(或者不是,如果它导致缓存未命中指令缓存)。但是你确实在堆栈上节省了一些空间。


您可以释放通用寄存器。这只有好处。

在regcall / fastcall中,这是一个额外的寄存器,您可以在其中存储函数的参数。因此,如果您的函数需要七个(在x86上;在大多数其他体系结构上更多)或更多参数(包括this),则第七个参数仍然适合寄存器。同样,更重要的是,也适用于局部变量。数组和大对象不适合寄存器(但指向它们的指针),但如果你的函数使用七个不同的局部变量(包括计算复杂表达式所需的临时变量),编译器很可能会生成更小的代码。较小的代码意味着较低的指令缓存占用空间,这意味着降低了错失率,从而减少了内存访问(但Intel Atom has a 32K instruction cache,这意味着您的代码可能无论如何都适合。)

x86架构具有[BX/BP/SI/DI][BX/BP + SI/DI]寻址模式。这使得BP寄存器对于缩放数组索引非常有用,特别是如果数组指针位于SI或DI寄存器中。两个偏移寄存器优于一个。

利用一个寄存器可以避免内存访问,但是如果一个变量值得存储在一个寄存器中,那么它很可能会在L1缓存中存活得很好(特别是因为它将在堆栈中)。移动到缓存或从缓存移动仍然是成本,但由于现代CPU做了很多移动优化和并行化,因此L1访问可能与寄存器访问一样快。因此,不移动数据的速度效益仍然存在,但不是很大。我可以很容易地想象CPU完全避免数据缓存,至少就读取而言(并且可以并行写入缓存)。

使用的寄存器是需要保留的寄存器。如果要在再次使用它之前将其推送到堆栈,则不值得在寄存器中存储太多。在keep-by-caller调用约定(例如上面的那个)中,这意味着寄存器作为持久存储在一个很多调用其他函数的函数中并不那么有用。

另请注意,x86具有用于浮点寄存器的单独寄存器空间,这意味着浮点数无法使用BP寄存器而无需额外的数据移动指令。只有整数和内存指针。


通过省略帧指针而失去的是可调试性。 This answer显示原因:

如果代码崩溃,则生成堆栈跟踪所需的所有调试器都是:

    PUSH FP      ; log the current frame pointer as well
$1: CALL log_FP  ; log the frame pointer currently on stack
    LEAVE        ; pop the frame pointer to get the next one
    CMP [FP+4],0
    JNZ $1       ; until the stack cannot be popped (the return address is some specific value)

如果代码在没有帧指针的情况下崩溃,调试器可能无法生成堆栈跟踪,因为它可能不知道(即,它需要定位函数入口/出口点)需要从中减去多少堆栈指针。如果调试器不知道帧指针未被使用,它甚至可能自行崩溃。