我可以阅读有关此类事物的资源
参见Agner Fog's microarch pdf,以及他的优化组装指南。 x86 标签 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。
但是,英特尔 P6 系列 CPU(PPro/PII 到 Nehalem)具有有限的寄存器读取端口,因此最好支持读取您刚刚编写的寄存器。如果%rbx 没有在最后几个周期中写入,则必须在mov 和imul uops 经历重命名和发布阶段(RAT)时从永久寄存器文件中读取它。
如果他们不是同一组 4 人的一部分,那么他们每个人都需要单独阅读 %rbx。由于 Core2/Nehalem 中的寄存器文件只有 3 个读取端口,因此问题组(四重奏,正如 Agner Fog 所说的那样)停滞不前,直到从寄存器文件中读取所有未写入的输入寄存器值(每个周期 3 个,或Core2 上的 2 是 3 个寄存器中没有一个是寻址模式中的索引寄存器)。
有关详细信息,请参阅Agner Fog's microarch pdf 第 8.8 节。 Core2 部分回溯到 PPro 部分。 PPro 有一个 3 宽的管道,所以在该部分中,Agner 谈论的是三重奏,而不是四重奏。
如果mov 和imul 一起发布,它们都共享%rbx 的相同读取。在 Core2/Nehalem 上发生这种情况的可能性为 4 分之三。
仅在您提到的第一个序列之间进行选择对于 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
如果您要依赖最近写入的寄存器,您不妨仅使用最近写入的寄存器。
英特尔 Sandybridge 系列使用物理寄存器文件(如 AMD Bulldozer 系列),并且没有寄存器读取停顿。
Ivybridge(第 2 代 Sandybridge)和更高版本还在寄存器重命名时处理 mov reg,reg,具有零延迟且没有执行单元。这意味着无论您是使用 rbx 还是 rcx 都与关键路径长度无关。
但是,AMD Bulldozer 系列只能在其重命名阶段处理 xmm 寄存器移动;整数寄存器移动仍然有 1c 的延迟。
如果延迟是循环每次迭代周期的限制因素,那么可能仍然值得关注 mov 属于哪个依赖链。
如何对此进行基准测试
我认为您可以将在 Core2 上具有寄存器读取停顿的微基准与 imul %rbx, %rcx 组合在一起,但不能与 imul %rcx, %rcx 组合在一起。然而,这将需要一些试验和错误才能让mov 和imul 在不同的组中发布,除非你真的很有创意,否则可能会有一些看起来像人工的周围代码,它们只是为了读取大量寄存器而存在。 (例如lea (%rsi, %rdi, 1), %eax,甚至add (%rsi, %rdi, 1), %eax(它必须读取所有三个寄存器,并且在core2/nehalem 上进行微熔丝,因此它只需要一个问题组中的1 个uop 插槽。(它doesn't micro-fuse on SnB-family))。