【问题标题】:In x86 assembly, is it better to use two separate registers for imul?在 x86 汇编中,对 imul 使用两个单独的寄存器会更好吗?
【发布时间】:2016-10-21 07:09:24
【问题描述】:

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

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

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

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

【问题讨论】:

    标签: performance assembly x86 micro-optimization


    【解决方案1】:

    我可以阅读有关此类事物的资源

    参见Agner Fog's microarch pdf,以及他的优化组装指南。 标签 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 没有在最后几个周期中写入,则必须在movimul 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 谈论的是三重奏,而不是四重奏。


    如果movimul 一起发布,它们都共享%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 组合在一起。然而,这将需要一些试验和错误才能让movimul 在不同的组中发布,除非你真的很有创意,否则可能会有一些看起来像人工的周围代码,它们只是为了读取大量寄存器而存在。 (例如lea (%rsi, %rdi, 1), %eax,甚至add (%rsi, %rdi, 1), %eax(它必须读取所有三个寄存器,并且在core2/nehalem 上进行微熔丝,因此它只需要一个问题组中的1 个uop 插槽。(它doesn't micro-fuse on SnB-family))。

    【讨论】:

      【解决方案2】:

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

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

      【讨论】:

      • 这是一种非常模糊的表达方式“我不这么认为”并链接到一些有用的 wiki 文章。寄存器重命名甚至不参与其中:即使没有重命名,寄存器读取也会在写回之前发生。这个建议的代码序列没有多个重复使用相同架构寄存器的 dep 链(这就是寄存器重命名发挥其魔力并打破依赖关系的时候)。
      • 无论如何,这不得不投反对票,因为它实际上在两个方面都是错误的: 1. 像 Nehalem 这样的英特尔 P6 系列 CPU 仍然很普遍,并且确实具有有限的寄存器读取端口。 2. 循环携带的依赖链的长度对于乱序执行仍然很重要。两个 OPs 序列具有相同的延迟,但也有可能。如果您可以减少延迟而不会使其他任何事情变得更糟,那么您应该这样做。如果您首先是手写 asm(或读取编译器输出,或编写编译器),那么一切都可能很重要。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-11-01
      • 2013-04-15
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多