如果您关心性能,pusha / popa 几乎没有用处。它们仅在以牺牲速度为代价优化代码大小时才有用,例如保存/恢复函数周围的寄存器。但是对于非void 函数来说非常不方便,因为它们会重新加载 all 寄存器,因此您必须将返回值存储在内存中(例如,在将加载到 eax 的堆栈槽上,或在popad之后重新加载的其他地方)。
只推送需要保存的寄存器,或者你想作为函数参数传递的寄存器。或者,在inline-assembly 中,只需让编译器通过为任何临时寄存器声明"=r"(dummy1) 虚拟输出操作数来为您管理寄存器,或者在特定寄存器上使用clobbers。通常,编译器可以选择可以让您在不保存的情况下破坏的寄存器。 (或者在笨重的 MSVC 风格的内联 asm 中,编译器无法为您分配寄存器,因此您必须手动选择。编译器会解析您的 asm 以查找 clobber。)
您通常不需要保存/恢复eax;为了性能,如果您首先无法计算esi 中的值,您可能应该mov esi, eax / 调用/使用esi 中的值。 即对需要在call 中存活的值使用调用保留寄存器,因此重要值的存储/重新加载不在关键路径上。相反,存储/重新加载位于调用者的调用保留寄存器之一的关键路径上,您(或编译器)push/pop 在整个函数周围,在任何循环之外。
详细了解call-preserved vs. call-clobbered registers 以及保存/恢复应该如何正常工作。什么是好的调用约定,例如how x86-64 System V was designed 和 this Q&A 关于应该在寄存器中传递多少个参数,以及为什么不将 XMM 寄存器用于整数参数。当然,辅助函数可以使用自定义调用约定。
pusha / popa 在大多数 CPU 上都很慢
即使您确实想要推送所有 8 个整数寄存器(包括 esp!),在现代 CPU 上使用 8 个单独的 push 指令实际上更快。 pusha/popa 是微编码的,which can be a problem for the front-end。 (虽然 8 条单字节指令对于 uop-cache 也可能是个问题。但在实际代码中,您通常只需要推送几个寄存器,而不是全部。)
如果您正在针对过时的 CPU(如原始有序 Pentium 和 Pentium II/III)进行优化,pusha/popa 与 8 个 push r 或 8 个 pop r 一样快,而且实际上更少的微指令,因为它们没有堆栈引擎来消除 ESP 更新微指令。
来自Agner Fog's instruction tables:现代 CPU 具有单微指令 push reg 和 pop reg,因为编译器一直使用这些指令,因此对性能很重要。 push/pop 吞吐量通常与存储/加载吞吐量相匹配(通常每个时钟 1 个存储或每个时钟 2 个加载)。但是pusha / popa 不被编译器使用,因此 CPU 设计人员没有特别的支持来使它们变得更快。 popa 如果只是 运行 popa,吞吐量将限制为每个时钟仅 1 个负载。 (我认为在 Intel CPU 上,测量性能最可能的解释是 popa 不使用堆栈引擎,因此它依赖于 esp 成为瓶颈。)
英特尔:
- Skylake:
pusha:11 微指令,8c 吞吐量。 popa:18 微指令/8c 吞吐量。
- Sandybridge:
pusha:16 微指令/8c 吞吐量。 popa:18 微指令/9c 吞吐量。
- Nehalem:
pusha:18 微指令/8c 吞吐量。 popa:10 微秒/8c 吞吐量。
- Silvermont/KNL:
pusha:10 uop / 10c 吞吐量。 popa:17 微指令/14c 吞吐量。
- Pentium4:
pusha:4/10 微指令/19c 吞吐量。 popa:4/16 uops / 14c 吞吐量。
- P5 Pentium 1 / MMX:5-9 个周期,不可配对。 “如果 SP 可被 4 整除,则为 9(不完美配对)。”
AMD:pusha/popa 在某些 AMD CPU,尤其是 K8 上的表现出奇的好。
- 锐龙:
pusha:9 微指令,8c 吞吐量。 popa:9 微指令,4c 吞吐量。 (与 Intel 不同,AMD 的新设计 popa 不比 pop 差 8 倍。)
- Jaguar:
pusha:9 微指令/8c 吞吐量。 popa:9 微指令/8c 吞吐量。 (Jaguar 通常每个时钟只能执行一次加载。)
- 打桩机:
pusha:9 uops / 9c 吞吐量。 popa:14 微指令/8c 吞吐量。 (Agner 将 Bulldozer 系列的常规 pop reg 吞吐量列为每时钟 1 个,尽管我认为它们确实有一个堆栈引擎并且每个时钟可以执行 2 个负载。也许堆栈引擎一次只能处理一个堆栈指令?)
- K8:
pusha:9 微指令/4c 吞吐量!! (IDK 这怎么可能,要么这是表中的错误或拼写错误,要么 K8 合并了 32 位寄存器并进行了四个 64 位存储)。 popa:9 微指令/4c 吞吐量。这些数字似乎是真实的:InstLatx86 measurements 同意 Clawhammer(第一代 K8 微架构)上 pushad / popad 的 4c 吞吐量。很明显,AMD 在优化 pushad 方面付出了一些努力。
你标记了这个inline-assembly。通常你应该避免在 inline-asm 中使用call,这样 C 编译器就会知道这个调用。
让编译器担心寄存器;只需告诉它您修改了哪些(GNU C asm("..." ::: "eax", "ecx") 或其他),或者在 MSVC 样式的内联汇编中,它会解析您的汇编并知道写入了哪些寄存器。如果这包括任何保留调用的寄存器,编译器将在整个函数的开始/结束处保存/恢复这些寄存器,即使 asm 语句处于循环中也是如此。 (它可能需要在 asm 语句或块之前/之后溢出和/或重新加载一些本地变量,但会使用 mov,而不是 push/pop。)