【问题标题】:Fastest Offset Read for a Small Array小阵列的最快偏移读取
【发布时间】:2020-05-19 08:28:45
【问题描述】:

为了速度,我想读取第 9 个寄存器中的值引用的 8 个寄存器之一。 我看到的最快方法是使用 3 个条件跳转(检查第 9 位中的 3 位 登记)。这应该比使用偏移执行此操作的标准方法具有更短的延迟 内存读取,但这仍然需要至少 6 个时钟周期(至少一个测试加一个 有条件的 jmp 每比特检查)。

是否有任何商业 CPU(最好是 x86/x64)具有执行此“偏移寄存器”的内在功能 读取”只有一个时钟周期的延迟?

理论上,优化的 CPU 可以通过一次加法和一次移动来完成此操作,因此需要两个或一个时钟 周期似乎很容易......是否存在架构不关心超速的一般原因 为一个小数组增加一个偏移读取?

【问题讨论】:

  • 如今将 CPU 寄存器视为一个数组确实不是一种常见的方法。我知道的最后一个允许这样做的架构是 PDP11,它在 80 年代后期消失了。为什么不像其他数组一样将数组放入某个内存位置?
  • 通过变量索引访问寄存器或多或少会破坏寄存器重命名,因此对于快速处理器来说这是一个非常不可能的功能。您可以使用 VPERMD 对 YMM 寄存器进行某种索引,也许这对于您的用例来说仍然可以?
  • @fuz AVR 也可以做到这一点;寄存器被映射到地址空间。
  • CPU 没有 intriniscs;编译器具有内在函数。也许您的意思是“机器指令”或“寻址模式”(编译器可能具有内在的)?正如之前的评论者所说,大多数 ISA 不能使用运行时索引来索引寄存器,只能使用嵌入在指令机器代码中的索引。 (所以寄存器获取可以在解码后的任何时间发生,而不需要任何额外的间接作为 ISA 的一部分。)
  • 自我修改代码比仅仅从数据缓存中刷新一行要昂贵得多;在现代 x86 上,它会刷新整个管道(性能计数器事件 machine_clears.smc)。如果你 JIT 一次并运行多次,那很好,如果你每次都这样做,那就是垃圾。在现代 Intel 上,存储转发延迟只有大约 3 到 5 个周期,类似于 4 或 5 个周期的 L1d 负载使用延迟。在需要低延迟数组索引的地方,您试图解决的更大问题是什么?你可以用 AVX2 shuffle 来解决它吗?这是 3c 延迟(加上来自整数 regs 的索引的 vmovd 更多)

标签: performance assembly x86 cpu-architecture intrinsics


【解决方案1】:

如今,将 CPU 寄存器视为数组确实不是一种常见的方法。我知道的最后一个允许这样做的架构是 PDP11,它在 80 年代后期消失了。为什么不像其他数组一样将数组放入某个内存位置?

也就是说,您可以使用计算跳转。这也将数据依赖项(索引寻址模式)替换为控制依赖项,因此乱序 exec 无需等待索引输入甚至准备好就可以开始运行使用最终 RAX 的代码。当然,这假设 正确 分支预测,如果索引经常更改,这是不太可能的。一个分支预测错误会花费许多周期的小工作来完成,但是在 L1d 缓存中命中的负载的小延迟很容易与独立工作重叠。

吞吐量成本高于内存中的数组:一些地址计算、一次跳转、一次移动和ret,而不仅仅是mov,甚至是具有索引寻址模式的内存操作数。

要内联此代码,只需将 jmp *%rax 替换为 call *%rax,这会花费另一个 uop。或者将 ret 指令替换为 jmp 到底部的标签,并将跳转表的步幅增加到 8 以考虑更长的编码。

    # select a register from r8...r15 according to the value in rdi
select:
    lea labels-4*8(%rip),%rax # rdi = 8 is the first jump table entry
    lea (%rax,%rdi,4),%rax    # pointer to the appropriate entry
    jmp *%rax                 # computed jump

    .align 4
labels:
    mov %r8, %rax
    ret

    .align 4
    mov %r9, %rax
    ret

    .align 4
    mov %r10, %rax
    ret

    .align 4
    mov %r11, %rax
    ret

    .align 4
    mov %r12, %rax
    ret

    .align 4
    mov %r13, %rax
    ret

    .align 4
    mov %r14, %rax
    ret

    .align 4
    mov %r15, %rax
    ret

虽然这可能比三个条件跳转更快(取决于访问模式),但它肯定不会优于仅使用数组。

【讨论】:

  • 好主意。我希望有一种 C++ 方式可以生成这样的程序集。另外,我不敢相信(还没有?)这会比随机访问的 L1 缓存慢。
  • @bobuhito Agner 的表格对 Skylake X 是这样说的。您的里程可能会有所不同。如果您告诉我们您尝试使用这种方法实现什么算法,我们可能会提出更好的解决方案,打破“将寄存器用作数组”的想法。
  • @bobuhito:是的,在 SnB 系列上,L1d 负载使用延迟为 4 个周期(最佳情况)。或者在 pointer-chasing special case 之外的 5 个周期,用于值来自另一个负载的简单寻址模式。但是核心不会在这 5 个周期内停止!!几乎总是有一些独立的工作要做。 负载吞吐量为每个周期 2 个(在现代 x86、AMD 和 Intel 上)。
  • @bobuhito 嗯...我会为此使用状态机。您打算如何处理其他不增加的值?显然,这不是全部。也许发布您想要实现的整个操作的一些伪代码(而不仅仅是您想要使用间接寻址的微小部分)。
  • @bobuhito 您的问题描述对于我来说也有点抽象。如果您可以在您的问题中添加一段自包含的代码来展示您想要实现的目标,那么最好是这样。然后我可以尝试将其重写为快速汇编。如果您只想执行一些简单的计算,请考虑是否真的需要执行所有 256 次迭代,或者是否可以使用一些求和公式。
猜你喜欢
  • 1970-01-01
  • 2021-04-07
  • 2018-03-21
  • 1970-01-01
  • 2012-04-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多