在 x86 汇编中,为 imul 使用两个单独的寄存器是否更好?

In x86 assembly, is it better to use two separate registers for imul?

我想知道,主要是出于好奇,是否对一个操作使用相同的寄存器比使用两个更好。考虑到性能 and/or 其他问题,什么会更好?

mov %rbx, %rcx
imul %rcx, %rcx

mov %rbx, %rcx
imul %rbx, %rcx

任何有关如何对其进行基准测试的提示,或我可以阅读此类内容的资源,我将不胜感激,因为我是装配新手。

在现代处理器上,将一个寄存器用于源和目标以及使用两个不同的寄存器永远不会对性能产生任何影响。部分原因是 register renaming,如果性能存在差异,可以通过将其中一个寄存器更改为另一个寄存器并修改后续指令以使用新寄存器来解决它(您的处理器实际上有比指令集更多的寄存器有一种引用它们的方式,这样它就可以做这样的事情)。这也是由于流水线处理器实现的性质——源寄存器的内容在一个流水线阶段被读取,然后在另一个后续阶段被写入,这使得单个指令的寄存器使用很难或不可能导致任何一种像你担心的那样的互动。

如果一条指令引用其前一条指令中产生的值,则问题更大,但即使这样(通常)也可以通过 out-of-order execution 解决。

resources where I could read about this type of thing

参见 Agner Fog's microarch pdf, and his optimizing assembly guide. Also other links in the 标签 wiki(例如英特尔的优化手册)。


您没有提到的有趣选项是:

mov   %rbx, %rcx
imul  %rbx, %rbx     # doesn'y have to wait for mov to execute
# old value of %rbx is still available in %rcx

如果 imul 在关键路径上,并且 mov 具有非零延迟(例如在 AMD CPU 和 IvyBridge 之前的 Intel 上),这可能会更好。 imul 的结果将提前一个周期就绪,因为它不依赖于 mov.

的结果

但是,如果旧值在关键路径上而平方值不在,则情况更糟,因为它向关键路径添加了 mov

当然,这也意味着您必须跟踪一个事实,即您的旧变量现在位于不同的寄存器中,并且旧寄存器具有平方值。如果这是循环中的问题,请将其展开,这样您就可以得到循环顶部期望的结果。如果您希望这很容易,您可以使用编译器而不是手动优化 asm。


但是,Intel P6 系列 CPU(PPro/PII 到 Nehalem)具有 有限的寄存器读取端口 ,因此最好支持读取寄存器写了。如果 %rbx 没有在最后几个周期中写入,则当 movimul 微指令通过重命名和发布阶段(RAT ).

如果他们不是同一组 4 人的一部分,那么他们每个人都需要单独阅读 %rbx。由于 Core2/Nehalem 中的寄存器文件只有 3 个读取端口,问题组(四重奏,如 Agner Fog 所说)会停止,直到从寄存器文件中读取所有它们最近未写入的输入寄存器值(每个周期 3 个) , 或者 Core2 上的 2 是 none 3 个 regs 是寻址模式中的索引 regs).

有关完整的详细信息,请参阅 Agner Fog's microarch pdf 第 8.8 节。 Core2 部分参考 PPro 部分。 PPro 有一个 3 宽的流水线,所以在那个部分 Agner 谈论的是三胞胎,而不是四胞胎。


如果 movimul 一起发出,则它们共享相同的读取 %rbx。 Core2/Nehalem.

有四分之三的几率发生这种情况

对于 Intel P6 系列 CPU,仅在您提到的第一个序列之间进行选择比第二个序列具有明显(但通常很小)的优势。其他 CPU 没有区别,AFAIK,所以选择是显而易见的。

mov   %rbx, %rcx
imul  %rcx, %rcx     # uses only the recently-written rcx; can't contribute to register-read stalls

两全其美:

mov   %rbx, %rcx
imul  %rbx, %rcx     # can't execute until after the mov, but still reads a potentially-old register

如果您要依赖最近写入的寄存器,您不妨使用 个最近写入的寄存器。


Intel Sandybridge 系列使用物理寄存器文件(如 AMD Bulldozer 系列),并且没有寄存器读取停顿。

Ivybridge(第 2 代 Sandybridge)及更高版本也在寄存器重命名时处理 mov reg,reg,零延迟且无执行单元。这意味着就关键路径长度而言,您使用 rbx 还是 rcx 并不重要。

但是,AMD Bulldozer 系列只能在其重命名阶段处理 xmm 寄存器移动;整数寄存器移动仍有 1c 延迟。

如果延迟是循环每次迭代周期的限制因素,那么 mov 是哪个依赖链的一部分仍然可能值得关注。


how to benchmark this

我认为您可以将在 Core2 上具有寄存器读取停顿的微基准与 imul %rbx, %rcx 放在一起,但不能与 imul %rcx, %rcx 放在一起。然而,这将需要一些试验和错误才能让 movimul 在不同的组中发布,除非你觉得真的很有创意,否则可能会有一些看起来很人工的周围代码,它们只存在于阅读很多寄存器。 (例如 lea (%rsi, %rdi, 1), %eax,甚至 add (%rsi, %rdi, 1), %eax(它必须读取所有三个寄存器,并在 core2/nehalem 上进行微熔断,因此它在一个问题组中只占用 1 个 uop 插槽。(它 doesn't micro-fuse on SnB-family)).