如果你关心性能,pusha
/ popa
几乎没有用处。它们仅在以牺牲速度为代价来优化代码大小时才有用,例如保存/恢复函数周围的寄存器。但对于非人来说非常不方便void
函数,因为它们会重新加载all寄存器,因此您必须将返回值存储在内存中(例如,通过将被加载到的堆栈槽)eax
,或其他地方之后重新加载popad
).
只压入需要保存的寄存器,或者您想要作为函数参数传递。或者,在内联汇编 /questions/tagged/inline-assembly,只需让编译器通过声明来为您管理寄存器"=r"(dummy1)
任何临时寄存器的虚拟输出操作数,或在特定寄存器上使用 clobber。通常,编译器可以选择可以让您破坏而不保存的寄存器。 (或者在笨重的 MSVC 风格的内联汇编中,编译器无法为您分配寄存器,因此您必须手动选择。编译器会解析您的汇编以查找破坏者。)
您通常不需要保存/恢复eax
;为了性能你应该mov esi, eax
/调用/使用中的值esi
,如果您无法计算其中的值esi
首先。即使用调用保留寄存器来保存需要保存的值call
,因此重要值的存储/重新加载不在关键路径上。相反,存储/重新加载位于您(或编译器)调用者的调用保留寄存器之一的关键路径上push
/pop
围绕整个函数,在任何循环之外。
查看更多关于调用保留寄存器与调用破坏寄存器 https://stackoverflow.com/questions/9268586/what-are-callee-and-caller-saved-registers/56178078#56178078以及保存/恢复通常如何进行。以及什么是良好的调用约定,例如x86-64 System V 是如何设计的 https://stackoverflow.com/questions/4429398/why-does-windows64-use-a-different-calling-convention-from-all-other-oses-on-x86/35619528#35619528,并且本次问答 https://stackoverflow.com/questions/33707228/why-not-store-function-parameters-in-xmm-vector-registers关于应该在寄存器中传递多少个参数,以及为什么不使用 XMM 寄存器来传递整数参数。当然,辅助函数可以使用自定义调用约定。
pusha
/ popa
在大多数 CPU 上都很慢
即使您确实想推送所有 8 个整数寄存器(包括esp
!),使用8个独立的push
现代 CPU 上的指令实际上更快。 Pusha/popa 是微编码的,这对于前端来说可能是一个问题 https://stackoverflow.com/questions/26907523/branch-alignment-for-loops-involving-micro-coded-instructions-on-intel-snb-famil。 (尽管 8 个单字节指令也可能对 uop 缓存造成问题。但在实际代码中,您通常只需要推送几个寄存器,而不是全部。)
如果您正在针对过时的 CPU(例如原始的有序 Pentium 和 Pentium II/III)进行优化,则 Pusha/popa 的速度可达 8push r
or 8 pop r
,实际上更少的微指令,因为他们没有堆栈引擎来消除 ESP 更新微指令。
From Agner Fog 的说明书 http://agner.org/optimize/:现代 CPU 具有单微指令push reg
and pop reg
,因为编译器始终使用这些指令,因此对性能很重要。推送/弹出吞吐量通常与存储/加载吞吐量相匹配(通常每个时钟 1 次存储或每个时钟 2 次加载)。但pusha
/ popa
编译器不使用它们,因此 CPU 设计者没有特殊的支持来提高它们的速度。popa
吞吐量仅限于每个时钟 1 个负载,如果just跑步popa
。 (我认为在 Intel CPU 上,测量性能最可能的解释是popa
不使用堆栈引擎,因此它的瓶颈在于对esp
.)
Intel:
- 天湖:
pusha
:11 uop,8c 吞吐量。popa
:18 uops / 8c 吞吐量。
- 珊迪大桥:
pusha
:16 uops / 8c 吞吐量。popa
:18 uops / 9c 吞吐量。
- 尼哈勒姆:
pusha
:18 uops / 8c 吞吐量。popa
:10 uops / 8c 吞吐量。
- 西尔弗蒙特/KNL:
pusha
:10 uops / 10c 吞吐量。popa
:17 uops / 14c 吞吐量。
- 奔腾4:
pusha
:4/10 uops / 19c 吞吐量。popa
:4/16 uops / 14c 吞吐量。
- P5 Pentium 1 / MMX:5-9 个周期,不可配对。 “如果 SP 能被 4 整除,则为 9(不完美配对)。”
AMD: pusha
/popa
在某些 AMD CPU 上表现出奇的好,尤其是 K8。
- Ryzen:
pusha
:9 uop,8c 吞吐量。popa
: 9 个微指令,4c 吞吐量。 (与英特尔不同的是,AMD 的新设计popa
不低于8倍pop
.)
- Jaguar:
pusha
:9 uops / 8c 吞吐量。popa
:9 uops / 8c 吞吐量。 (Jaguar 通常每个时钟只能执行一次负载。)
- 打桩机:
pusha
:9 uops / 9c 吞吐量。popa
:14 uops / 8c 吞吐量。 (阿格纳列出了常规pop reg
Bulldozer 系列的吞吐量为每个时钟 1,尽管我认为他们确实有一个堆栈引擎并且每个时钟可以执行 2 个负载。也许堆栈引擎一次只能处理一条堆栈指令?)
- K8:
pusha
:9 uops / 4c 吞吐量!! (不知道这是怎么可能的,要么这是表中的错误或拼写错误,要么 K8 合并了 32 位寄存器并进行了四个 64 位存储)。popa
:9 uops / 4c 吞吐量。这些数字看起来确实是真实的:InstLatx86 测量 http://users.atw.hu/instlatx64/AuthenticAMD0000F4A_K8_Clawhammer_InstLatX86.txt同意 4c 吞吐量pushad
/ popad
Clawhammer(第一代 K8 微架构)。很明显AMD在优化上付出了一些努力pushad
.
您标记了此内联汇编 /questions/tagged/inline-assembly。通常你应该避免使用call
在内联汇编中,因此 C 编译器知道该调用。
让编译器关心寄存器;只需告诉它你修改了哪些(GNU Casm("..." ::: "eax", "ecx")
或其他),或者在 MSVC 风格的内联汇编中,它会解析您的汇编并知道写入了哪些寄存器。如果其中包含任何调用保留的寄存器,编译器将在整个函数的开始/结束处保存/恢复这些寄存器,即使 asm 语句处于循环中也是如此。 (它可能需要在asm语句或块之前/之后溢出和/或重新加载一些本地变量,但会使用mov,而不是push/pop。)