为什么使用push/pop而不是sub和mov



当我在https://godbolt.org,我注意到编译器生成这样的代码是非常常见的:

push    rax
push    rbx
push    rcx
call    rdx
pop     rcx
pop     rbx
pop     rax

我知道每个pushpop都做两件事:

  1. 将操作数移动到堆栈空间
  2. 递增/递减堆栈指针(rsp(

所以在我们上面的例子中,我假设CPU实际上正在执行12个操作(6个移动,6个加法/减法(,不包括call。把加法/减法组合起来不是更有效吗?例如:

sub rsp, 24
mov [rsp-24], rax
mov [rsp-16], rbx
mov [rsp-8], rcx
call    rdx
mov rcx, [rsp-8]
mov rbx, [rsp-16]
mov rax, [rsp-24]
add rsp, 24

现在只有8个操作(6次移动,2次添加/子操作(,不包括call。为什么编译器不使用这种方法?

如果使用-mtune=pentium3-mtune=pentium-m之前的版本进行编译,GCC将像您想象的那样执行代码生成,因为在那些旧CPU上,push/pop确实会解码到堆栈指针上的单独ALU操作以及加载/存储。(您必须使用-m32-march=nocona(64位P4 Prescott(,因为这些旧CPU也不支持x86-64(。为什么gcc使用movl而不是push来传递函数args?

但Pentium-M在前端引入了一个"堆栈引擎",消除了堆栈操作中的堆栈调整部分,如推送/调用/重新设置/弹出。它有效地以零延迟重命名堆栈指针。请参阅Agner Fog的微芯片指南和Sandybridge微体系结构中的堆栈引擎是什么?

作为一种普遍趋势,任何在现有二进制文件中广泛使用的指令都会激励CPU设计者使其快速运行。例如,奔腾4试图让所有人停止使用INC/DEC;但没有奏效;当前的CPU比以往任何时候都能更好地进行部分标志重命名。现代x86晶体管和功率预算可以支持这种复杂性,至少对于大核心CPU(而不是Atom/Silvermont(来说是这样。不幸的是,我认为sqrtsscvtsi2ss之类的指令(在目的地上(的错误依赖性没有任何希望。


在类似add rsp, 8的指令中显式使用堆栈指针需要Intel CPU中的堆栈引擎插入一个同步uop来更新寄存器的无序后端值。如果内部偏移过大,也是如此。

事实上,在现代CPU上,pop dummy_register的效率比add rsp, 8add esp,4高,因此编译器通常会使用它来弹出一个带有默认调优的堆栈插槽,例如-march=sandybridge。为什么这个函数将RAX作为第一个操作推送到堆栈?

另请参阅什么C/C++编译器可以使用pushpop指令来创建局部变量,而不是只增加esp一次?re:使用push初始化堆栈上的局部变量,而不是sub rsp, n/mov。在某些情况下,这可能是一个胜利,尤其是对于具有小值的代码大小,但编译器不能做到这一点


此外,不,GCC/clang不会生成与您所显示的完全的代码。

如果他们需要在函数调用周围保存寄存器,他们通常会使用mov对内存进行保存。或者mov到他们保存在函数顶部的调用保留寄存器,并将在最后恢复。

除了传递堆栈参数之外,我从未见过GCC或clang在函数调用之前推送多个调用失败的寄存器。之后绝对不会多次弹出以恢复到相同(或不同(的寄存器中。函数内部的溢出/重新加载通常使用mov。这避免了在循环中进行推送/弹出的可能性(除了将堆栈参数传递给call(,并允许编译器进行分支,而不必担心将推送与弹出相匹配。此外,它还降低了堆栈展开元数据的复杂性,该元数据必须为每个移动RSP的指令都有一个条目。(使用RBP作为传统帧指针时,在指令计数与元数据和代码大小之间进行了有趣的权衡。(

类似的,您的代码生成中可以看到保留调用的寄存器+一个小函数中的一些reg reg移动,该函数刚刚调用了另一个函数,然后返回了一个__int128,这是寄存器中的一个函数arg。因此,需要保存传入的RSI:RDI,以便在RDX:RAX中返回。

或者,如果在非内联函数调用之后存储到全局或通过指针,编译器还需要保存函数参数,直到调用之后。

最新更新