gpt4 book ai didi

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

转载 作者:行者123 更新时间:2023-12-02 19:58:49 25 4
gpt4 key购买 nike

当我在 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 次添加/替换),不包括调用。将添加/替换组合起来不是更有效吗?例如:

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 个添加/替换),不包括调用。为什么编译器不使用这种方法?

最佳答案

如果您使用 -mtune=pentium3 或早于 -mtune=pentium-m 的内容进行编译,GCC 执行代码生成,如下所示你可以想象,因为在那些旧的 CPU 上,push/pop 确实解码为堆栈指针上的单独 ALU 操作以及加载/存储。 (您必须使用 -m32-march=nocona (64 位 P4 Prescott),因为这些旧 CPU 也不支持 x86-64)。 Why does gcc use movl instead of push to pass function args?

但是 Pentium-M 在前端引入了一个“堆栈引擎”,消除了堆栈操作的堆栈调整部分,例如push/call/ret/pop。它有效地以零延迟重命名堆栈指针。请参阅Agner Fog's microarch guideWhat is the stack engine in the Sandybridge microarchitecture?

作为总体趋势,现有二进制文件中广泛使用的任何指令都会激励 CPU 设计人员提高其速度。例如,Pentium 4 试图让大家停止使用 INC/DEC;那不起作用; current CPUs do partial-flag renaming better than ever 。现代 x86 晶体管和功率预算可以支持这种复杂性,至少对于大核 CPU(不是 Atom/Silvermont)来说是这样。不幸的是,我认为对于 sqrtsscvtsi2ss 等指令的错误依赖(在目标上)没有任何希望。

<小时/>

在像add rsp, 8这样的指令中显式使用堆栈指针需要Intel CPU中的堆栈引擎插入同步微指令来更新寄存器的乱序后端值。如果内部偏移太大,则相同。

事实上,pop dummy_register 比现代上的 add rsp, 8add esp,4 更高效 CPU,因此编译器通常会使用它来通过默认调整或例如 -march=sandybridge 来弹出一个堆栈槽。 Why does this function push RAX to the stack as the first operation?

另请参阅What C/C++ compiler can use push pop instructions for creating local variables, instead of just increasing esp once?回复:使用 push 初始化堆栈上的局部变量,而不是 sub rsp, n/mov。在某些情况下,这可能是一个胜利,特别是对于值较小的代码大小,但编译器不会这样做。

<小时/>

另外,不,GCC/clang 不会生成与您所展示的完全相同的代码。

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

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

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

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

关于assembly - 为什么使用push/pop而不是sub和mov?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60872952/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com