x86 程序集 pushad/popad,速度有多快?
x86 Assembly pushad/popad, How fast it is?
我只是想在 x86 汇编中编写非常快速的基于计算的程序
但我需要在调用过程之前推送累加器、计数器和数据寄存器。手动推送它们更快:
push eax
push ecx
push edx
或者只是使用,
pushad
和 poping 一样。谢谢
如果您关心性能,pusha
/ popa
几乎没有用。它们仅在以牺牲速度为代价优化代码大小时才有用,例如to save/restore 围绕一个函数注册。但是对于非 void
函数来说非常不方便,因为它们会重新加载 all 寄存器,所以你必须将 return 值存储在内存中(例如,在堆栈槽上将加载到 eax
,或在 popad
之后重新加载的其他地方。
只压入需要保存的寄存器,或者你想作为函数参数传递的寄存器。或者,在 inline-assembly 中,只需为任何临时寄存器声明 "=r"(dummy1)
虚拟输出操作数,让编译器为您管理寄存器,或者在特定寄存器上使用 clobbers。通常,编译器可以选择可以让您在不保存的情况下破坏的寄存器。 (或者在笨重的 MSVC 风格的内联 asm 中,编译器无法为你分配寄存器,所以你必须手动选择。编译器解析你的 asm 以找到 clobbers。)
您通常不需要 save/restore eax
;为了提高性能,如果您一开始无法计算 esi
中的值,您可能应该 mov esi, eax
/ 调用 / 使用 esi
中的值。 即对需要在 call
中存活的值使用调用保留寄存器,因此重要值的 store/reload 不在关键路径上。相反,store/reload 位于调用者的调用保留寄存器之一的关键路径上,您(或编译器)push
/pop
围绕整个函数,在任何循环之外。
详细了解 call-preserved vs. call-clobbered registers and how saving/restoring should normally work. And what makes a good calling convention, e.g. how x86-64 System V was designed, and also 应在寄存器中传递多少个参数,以及为什么不对整数参数也使用 XMM 寄存器。当然,辅助函数可以使用自定义调用约定。
pusha
/ popa
在大多数 CPU 上都很慢
即使您确实想要压入所有 8 个整数寄存器(包括 esp
!),使用 8 个单独的 push
指令实际上在现代上更快CPU秒。 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
,实际上更少的 uops,因为他们没有堆栈引擎来消除 ESP 更新 uops。
来自 Agner Fog's instruction tables:现代 CPUs 有单 uop push reg
和 pop reg
,因为这些指令一直被编译器使用,因此对表现。 push/pop 吞吐量通常匹配 store/load 吞吐量(通常每个时钟 1 个存储或每个时钟 2 个加载)。但是 pusha
/ popa
没有被编译器使用,所以 CPU 设计者没有特殊的支持来使它们变快。 popa
如果 just 运行 popa
,则吞吐量仅限于每个时钟 1 个负载。 (我认为在 Intel CPUs 上,对测量性能最可能的解释是 popa
不使用堆栈引擎,因此它的瓶颈依赖于 esp
。)
英特尔:
- Skylake:
pusha
:11 微指令,8c 吞吐量。 popa
:18 uops / 8c 吞吐量。
- Sandybridge:
pusha
:16 uops / 8c 吞吐量。 popa
:18 uops / 9c 吞吐量。
- Nehalem:
pusha
:18 uops / 8c 吞吐量。 popa
:10 uops / 8c 吞吐量。
- Silvermont/KNL:
pusha
: 10 uops / 10c 吞吐量。 popa
:17 微指令/14c 吞吐量。
- Pentium4:
pusha
:4/10 uops / 19c 吞吐量。 popa
:4/16 微指令/14c 吞吐量。
- P5 Pentium 1 / MMX:5-9 周期,不可配对。 “9 如果 SP 可以被 4 整除(不完美的配对)。”
AMD:pusha
/popa
在某些 AMD CPUs 上出奇的好,尤其是 K8。
- Ryzen:
pusha
:9 微指令,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 吞吐量。 (Agner 将 Bulldozer 系列的常规 pop reg
吞吐量列为每个时钟 1 个,尽管我认为他们确实有一个堆栈引擎并且每个时钟可以执行 2 个负载。也许堆栈引擎一次只能处理一个堆栈指令? )
- K8:
pusha
:9 uops / 4c 吞吐量!! (我不知道这是怎么可能的,这是 table 中的错误或拼写错误,或者 K8 合并了 32 位寄存器并进行了四个 64 位存储)。 popa
:9 uops / 4c 吞吐量。这些数字看起来确实是真实的:InstLatx86 measurements 与 Clawhammer(第一代 K8 微体系结构)上 pushad
/ popad
的 4c 吞吐量一致。很明显,AMD 在优化 pushad
. 方面付出了一些努力
您标记了这个 inline-assembly。通常你应该避免在 inline-asm 中使用 call
,这样 C 编译器就会知道这个调用。
并且让编译器去操心寄存器;只需告诉它您修改了哪些(GNU C asm("..." ::: "eax", "ecx")
或其他),或者在 MSVC 样式的内联 asm 中,它会解析您的 asm 并知道写入了哪些寄存器。如果其中包含任何调用保留寄存器,编译器将 save/restore 那些位于整个函数 start/end 的寄存器,即使 asm 语句在循环中也是如此。 (它可能需要溢出 and/or 重新加载一些本地变量 before/after asm 语句或块,但会使用 mov,而不是 push/pop。)
我只是想在 x86 汇编中编写非常快速的基于计算的程序 但我需要在调用过程之前推送累加器、计数器和数据寄存器。手动推送它们更快:
push eax
push ecx
push edx
或者只是使用,
pushad
和 poping 一样。谢谢
如果您关心性能,pusha
/ popa
几乎没有用。它们仅在以牺牲速度为代价优化代码大小时才有用,例如to save/restore 围绕一个函数注册。但是对于非 void
函数来说非常不方便,因为它们会重新加载 all 寄存器,所以你必须将 return 值存储在内存中(例如,在堆栈槽上将加载到 eax
,或在 popad
之后重新加载的其他地方。
只压入需要保存的寄存器,或者你想作为函数参数传递的寄存器。或者,在 inline-assembly 中,只需为任何临时寄存器声明 "=r"(dummy1)
虚拟输出操作数,让编译器为您管理寄存器,或者在特定寄存器上使用 clobbers。通常,编译器可以选择可以让您在不保存的情况下破坏的寄存器。 (或者在笨重的 MSVC 风格的内联 asm 中,编译器无法为你分配寄存器,所以你必须手动选择。编译器解析你的 asm 以找到 clobbers。)
您通常不需要 save/restore eax
;为了提高性能,如果您一开始无法计算 esi
中的值,您可能应该 mov esi, eax
/ 调用 / 使用 esi
中的值。 即对需要在 call
中存活的值使用调用保留寄存器,因此重要值的 store/reload 不在关键路径上。相反,store/reload 位于调用者的调用保留寄存器之一的关键路径上,您(或编译器)push
/pop
围绕整个函数,在任何循环之外。
详细了解 call-preserved vs. call-clobbered registers and how saving/restoring should normally work. And what makes a good calling convention, e.g. how x86-64 System V was designed, and also
pusha
/ popa
在大多数 CPU 上都很慢
即使您确实想要压入所有 8 个整数寄存器(包括 esp
!),使用 8 个单独的 push
指令实际上在现代上更快CPU秒。 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
,实际上更少的 uops,因为他们没有堆栈引擎来消除 ESP 更新 uops。
来自 Agner Fog's instruction tables:现代 CPUs 有单 uop push reg
和 pop reg
,因为这些指令一直被编译器使用,因此对表现。 push/pop 吞吐量通常匹配 store/load 吞吐量(通常每个时钟 1 个存储或每个时钟 2 个加载)。但是 pusha
/ popa
没有被编译器使用,所以 CPU 设计者没有特殊的支持来使它们变快。 popa
如果 just 运行 popa
,则吞吐量仅限于每个时钟 1 个负载。 (我认为在 Intel CPUs 上,对测量性能最可能的解释是 popa
不使用堆栈引擎,因此它的瓶颈依赖于 esp
。)
英特尔:
- Skylake:
pusha
:11 微指令,8c 吞吐量。popa
:18 uops / 8c 吞吐量。 - Sandybridge:
pusha
:16 uops / 8c 吞吐量。popa
:18 uops / 9c 吞吐量。 - Nehalem:
pusha
:18 uops / 8c 吞吐量。popa
:10 uops / 8c 吞吐量。 - Silvermont/KNL:
pusha
: 10 uops / 10c 吞吐量。popa
:17 微指令/14c 吞吐量。 - Pentium4:
pusha
:4/10 uops / 19c 吞吐量。popa
:4/16 微指令/14c 吞吐量。 - P5 Pentium 1 / MMX:5-9 周期,不可配对。 “9 如果 SP 可以被 4 整除(不完美的配对)。”
AMD:pusha
/popa
在某些 AMD CPUs 上出奇的好,尤其是 K8。
- Ryzen:
pusha
:9 微指令,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 吞吐量。 (Agner 将 Bulldozer 系列的常规pop reg
吞吐量列为每个时钟 1 个,尽管我认为他们确实有一个堆栈引擎并且每个时钟可以执行 2 个负载。也许堆栈引擎一次只能处理一个堆栈指令? ) - K8:
pusha
:9 uops / 4c 吞吐量!! (我不知道这是怎么可能的,这是 table 中的错误或拼写错误,或者 K8 合并了 32 位寄存器并进行了四个 64 位存储)。popa
:9 uops / 4c 吞吐量。这些数字看起来确实是真实的:InstLatx86 measurements 与 Clawhammer(第一代 K8 微体系结构)上pushad
/popad
的 4c 吞吐量一致。很明显,AMD 在优化pushad
. 方面付出了一些努力
您标记了这个 inline-assembly。通常你应该避免在 inline-asm 中使用 call
,这样 C 编译器就会知道这个调用。
并且让编译器去操心寄存器;只需告诉它您修改了哪些(GNU C asm("..." ::: "eax", "ecx")
或其他),或者在 MSVC 样式的内联 asm 中,它会解析您的 asm 并知道写入了哪些寄存器。如果其中包含任何调用保留寄存器,编译器将 save/restore 那些位于整个函数 start/end 的寄存器,即使 asm 语句在循环中也是如此。 (它可能需要溢出 and/or 重新加载一些本地变量 before/after asm 语句或块,但会使用 mov,而不是 push/pop。)