如果你编译-mtune=pentium3
或早于-mtune=pentium-m
, GCC will像你想象的那样进行代码生成,因为在那些旧的 CPU 上,push/pop 确实解码为堆栈指针上的单独 ALU 操作以及加载/存储。 (你必须使用-m32
, or -march=nocona
(64位P4 Prescott)因为那些旧的CPU也不支持x86-64)。为什么gcc使用movl而不是push来传递函数参数?
但 Pentium-M 在前端引入了一个“堆栈引擎”,消除了堆栈操作的堆栈调整部分,例如推送/调用/返回/弹出。它有效地以零延迟重命名堆栈指针。看Agner Fog 的微架构指南 and Sandybridge 微架构中的堆栈引擎是什么?
作为总体趋势,现有二进制文件中广泛使用的任何指令都会激励 CPU 设计人员提高其速度。例如,Pentium 4 试图让大家停止使用 INC/DEC;那不起作用;当前的 CPU 比以往更好地进行部分标志重命名。现代 x86 晶体管和功率预算可以支持这种复杂性,至少对于大核 CPU(不是 Atom / Silvermont)来说是这样。不幸的是,我认为对于诸如以下指令的错误依赖关系(在目的地上)没有任何希望sqrtss
or cvtsi2ss
, 尽管。
在指令中显式使用堆栈指针,例如add rsp, 8
需要Intel CPU中的堆栈引擎插入同步微指令来更新寄存器的乱序后端值。如果内部偏移量太大,则相同。
In fact pop dummy_register
is more效率比add rsp, 8
or add esp,4
在现代 CPU 上,因此编译器通常会使用它来通过默认调整或使用-march=sandybridge
例如。为什么这个函数将RAX压入堆栈作为第一个操作?
也可以看看什么 C/C++ 编译器可以使用 push pop 指令来创建局部变量,而不是只增加 esp 一次?回复:使用push
初始化堆栈上的局部变量而不是sub rsp, n
/ mov
。在某些情况下,这可能是一个胜利,特别是对于值较小的代码大小,但编译器不会这样做。
另外,不,GCC / clang 不会生成这样的代码exactly就像你所展示的那样。
如果他们需要在函数调用周围保存寄存器,他们通常会使用mov
记忆。或者mov
到他们保存在函数顶部的调用保留寄存器,并将在最后恢复。
除了传递堆栈参数之外,我从未见过 GCC 或 clang 在函数调用之前推送多个被调用破坏的寄存器。并且绝对不会在之后多次弹出以恢复到相同(或不同)寄存器中。函数内部的溢出/重新加载通常使用 mov。这避免了在循环内推送/弹出的可能性(除了将堆栈参数传递给call
),并允许编译器进行分支,而不必担心推送与弹出的匹配。它还降低了堆栈展开元数据的复杂性,该元数据必须为移动 RSP 的每条指令都有一个条目。 (使用 RBP 作为传统帧指针时,指令数与元数据和代码大小之间的有趣权衡。)
某物like您的代码生成可以通过调用保留的寄存器+一些reg-reg在一个小函数中移动来看到,该函数只是调用另一个函数,然后返回一个__int128
那是寄存器中的函数arg。因此,需要保存传入的 RSI:RDI,以便以 RDX:RAX 形式返回。
或者,如果在非内联函数调用后存储到全局或通过指针,编译器还需要保存函数参数直到调用之后。