30

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

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

微软似乎表示添加帧指针 (/Oy-) 会使调试更容易,但真的是这样吗?

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

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

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

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

使用 Visual Studio 2010。

4

1 回答 1

43

简短回答:通过省略帧指针,

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

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

您将内存写入保存到缓存内存(堆栈),并且(理论上)在函数进入/退出时节省了一些时钟滴答,但您可以增加代码大小。除非您的函数很少经常执行(在这种情况下应该内联),否则这不应该引起注意。

您释放了一个通用寄存器。如果编译器可以使用该寄存器,它将生成更小且可能更快的代码。但是,如果大部分 CPU 时间都花在与主内存(甚至硬盘驱动器)进行通信上,那么省略帧指针并不能让您摆脱困境。

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


长答案:

典型的函数入口和出口是:

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),则第七个参数仍然适合寄存器。更重要的是,同样适用于局部变量。数组和大对象不适合寄存器(但指向它们的指针),但如果您的函数使用七个不同的局部变量(包括计算复杂表达式所需的临时变量),编译器可能会生成更小的代码. 更小的代码意味着更低的指令缓存占用空间,这意味着更低的未命中率,因此内存访问更少(但英特尔凌动有 32K 指令缓存,这意味着您的代码可能无论如何都适合)。

x86 体系结构具有[BX/BP/SI/DI][BX/BP + SI/DI]寻址模式。这使得 BP 寄存器成为缩放数组索引非常有用的地方,尤其是当数组指针驻留在 SI 或 DI 寄存器中时。两个偏移寄存器比一个好。

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

使用的寄存器是需要保存的寄存器。如果您在再次使用它之前无论如何都要把它推入堆栈,那么在寄存器中存储太多是不值得的。在按调用者保留的调用约定(例如上面的约定)中,这意味着寄存器作为持久存储在大量调用其他函数的函数中没有那么有用。

另请注意,x86 为浮点寄存器提供了单独的寄存器空间,这意味着无论如何,如果没有额外的数据移动指令,浮点数就无法使用 BP 寄存器。只有整数和内存指针可以。


省略帧指针会失去可调试性。这个答案说明了原因:

如果代码崩溃,调试器需要做的就是生成堆栈跟踪:

    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)

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

于 2012-10-22T08:06:19.073 回答