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

Jos*_*ise 2 x86 assembly x86-64 cpu-architecture micro-optimization

当我在https://godbolt.org上使用不同的编译器时,我注意到编译器生成这样的代码是很常见的:

push    rax
push    rbx
push    rcx
call    rdx
pop     rcx
pop     rbx
pop     rax
Run Code Online (Sandbox Code Playgroud)

我理解每个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
Run Code Online (Sandbox Code Playgroud)

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

Pet*_*des 7

如果您使用-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 在前端引入了一个“堆栈引擎”,它消除了堆栈操作的堆栈调整部分,如 push/call/ret/pop。它有效地以零延迟重命名堆栈指针。请参阅Agner Fog 的微架构指南Sandybridge 微架构中的堆栈引擎是什么?

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


在指令中显式使用堆栈指针add rsp, 8需要英特尔 CPU 中的堆栈引擎插入同步 uop 以更新寄存器的乱序后端值。如果内部偏移量太大,则相同。

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

另请参阅哪些 C/C++ 编译器可以使用 push pop 指令来创建局部变量,而不是仅仅增加一次 esp?回复:push用于初始化堆栈上的局部变量而不是sub rsp, n/ mov。在某些情况下,这可能是一种胜利,特别是对于具有小值的代码大小,但编译器不会这样做。


另外,不,GCC / clang 不会制作与您展示的完全一样的代码。

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

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

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

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