【问题标题】:x86 Assembly pushad/popad, How fast it is?x86 组装 pushad/popad,它有多快?
【发布时间】:2018-01-25 17:57:57
【问题描述】:

我只是想在 x86 汇编中制作非常快速的基于计算的程序 但我需要在调用程序之前推送累加器、计数器和数据寄存器。手动推送它们更快:

push eax
push ecx
push edx

或者只是使用,

pushad

和弹出一样。谢谢

【问题讨论】:

  • Agner Fog 的表格将 pusha/popa 在 skylake 上的微操作数略低于 20,甚至没有给出延迟数字。相比之下,每个push/pop 是一个寄存器操作数的 1-2 个微操作,2-3 个周期延迟,每个周期吞吐量可能有多个操作。
  • 有实际的理由不要乱用pushad/popad。您无法选择要推送和弹出哪些寄存器 - 要么全有,要么全无。
  • 如果您尝试创建某个程序的非常快速的汇编版本,您应该首先用 C 或 C++ 编写它,并以此为起点进行基准测试。无论您尝试什么,基准测试/分析都非常重要,理想情况下是在实际使用场景中,因此您还将对所有缓存/内存限制进行基准测试。人工基准非常容易编写+错误执行,得到的结果不代表代码的真实性能。无论如何,在没有分析的情况下进行性能编程是错误的做法,特别是在 x86 上,由于 x86 的非常复杂的性质,这几乎是不可能的。
  • push 和 pop 性能应该主要取决于内存访问时间,是吗?命中或未命中,如果有的话,什么级别的缓存。对于推送是否有消耗它的写缓冲区?写缓冲区满或有空间等有什么区别?
  • 只推送您需要推送的寄存器。您还可以推送一个寄存器并将其弹出到另一个寄存器。我不会推荐pusha/popa

标签: performance assembly x86 inline-assembly micro-optimization


【解决方案1】:

如果您关心性能,pusha / popa 几乎没有用处。它们仅在以牺牲速度为代价优化代码大小时才有用,例如保存/恢复函数周围的寄存器。但是对于非void 函数来说非常不方便,因为它们会重新加载 all 寄存器,因此您必须将返回值存储在内存中(例如,在将加载到 eax 的堆栈槽上,或在popad之后重新加载的其他地方)。

只推送需要保存的寄存器,或者你想作为函数参数传递的寄存器。或者,在 中,只需让编译器通过为任何临时寄存器声明"=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 designedthis 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 regpop 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-asm 中使用call,这样 C 编译器就会知道这个调用。

让编译器担心寄存器;只需告诉它您修改了哪些(GNU C asm("..." ::: "eax", "ecx") 或其他),或者在 MSVC 样式的内联汇编中,它会解析您的汇编并知道写入了哪些寄存器。如果这包括任何保留调用的寄存器,编译器将在整个函数的开始/结束处保存/恢复这些寄存器,即使 asm 语句处于循环中也是如此。 (它可能需要在 asm 语句或块之前/之后溢出和/或重新加载一些本地变量,但会使用 mov,而不是 push/pop。)

【讨论】:

  • 另外应该提到的是,PUSHA/POPA 不存在于 64 位模式中,因此将来将您的代码移植到这种架构会更加困难。
  • @vitsoft:我忽略了这一点,因为 x86-64 无论如何使用不同的调用约定,具有不同的调用保留和调用破坏寄存器。例如在 x86-64 SysV 调用约定中,仅有 2 个保留调用的非新寄存器是 RBX 和 RBP(不包括 RSP,它当然也是保留调用的)。所以无论如何你都必须重新审视你的推送/弹出(如果你真的想保存/恢复所有内容,至少将所有寄存器名称从eax更改为rax)。但是,是的,值得一提的是它已经过时了。
  • 当然,但是 x86-64 调用约定仅涉及 OS ABI 调用。如果我的例程是私有的,它可能会调用 ip
  • 对不起,这个愚蠢的 Android SO 客户端不允许我编辑或删除过早发送的 cmets ?
  • @vitsoft:是的,这是真的。您可以在 32 位模式下为私有帮助函数使用 register-arg 约定,并破坏或保留任何方便的内容。
猜你喜欢
  • 2016-09-06
  • 2015-11-21
  • 1970-01-01
  • 2012-03-03
  • 2020-04-30
  • 1970-01-01
  • 1970-01-01
  • 2012-03-07
  • 1970-01-01
相关资源
最近更新 更多