【问题标题】:Why does breaking the "output dependency" of LZCNT matter?为什么打破 LZCNT 的“输出依赖”很重要?
【发布时间】:2014-02-18 20:10:49
【问题描述】:

在对某些东西进行基准测试时,我测得的吞吐量比我计算的要低得多,我将其范围缩小到 LZCNT 指令(TZCNT 也会发生这种情况),如以下基准所示:

  xor ecx, ecx
_benchloop:
  lzcnt eax, edx
  add ecx, 1
  jnz _benchloop

还有:

  xor ecx, ecx
_benchloop:
  xor eax, eax  ; this shouldn't help, but it does
  lzcnt eax, edx
  add ecx, 1
  jnz _benchloop

第二个版本要快得多。不应该。 LZCNT 没有理由对其输出具有输入依赖性。与 BSR/BSF 不同,xZCNT 指令总是覆盖它们的输出。

我在 4770K 上运行它,所以 LZCNT 和 TZCNT 没有作为 BSR/BSF 执行。

这是怎么回事?

【问题讨论】:

  • 也许 lzcnt 不能被推测执行(它更新 CF、ZF) jnz (ZF != 0) 之后。而xor 打破了依赖链?但是由于add 无论如何都会淘汰以前的标志,如果是这种情况,我不会。
  • 只是为了确定:你能排除这是一个代码对齐问题,与lzcnt无关吗?
  • @PhiS 使用 3 字节 nop 而不是 xor eax, eax 让它再次变慢
  • gcc 4.9.2 中添加了“xor”解决方法:gcc.gnu.org/PR62011
  • 供未来访问者参考,这只是微架构勘误表(本质上是一个错误)。 LZCNT 没有理由对其输出具有输入依赖性,但确实如此。 POPCNT指令也有同样的bug,详见here

标签: performance assembly x86 cpu-architecture micro-optimization


【解决方案1】:

这只是您的 Intel Haswell CPU 和之前的几个1 CPU 的微架构的限制。从 Skylake-S(客户端)起,tzcntlzcnt 已修复此问题,但 popcnt 的问题仍然存在,直到它在 Cannon Lake 中得到修复。

在那些微架构上,tzcntlzcntpopcnt 的目标操作数被视为输入依赖项,尽管在语义上它不是。现在我怀疑这真的是一个“错误”:如果它只是一个意外的行为/疏忽,我希望它会在自引入以来已发布的几个新微架构之一中得到修复。

很可能是基于以下两个因素之一或两者的设计折衷:

  • popcntlzcnttzcnt 的硬件是 likely all shared 与现有的 bsfbsr 指令。现在bsfbsr do 依赖于先前的目标值在实践中2 对于所有位的特殊情况-零输入,因为英特尔芯片在这种情况下不会修改目的地。因此,组合硬件的最简单设计完全有可能导致在同一单元上执行的其他类似指令继承相同的依赖关系。

  • 绝大多数 x86 两个操作数 ALU 指令都依赖于目标操作数,因为它也用作源。这三个受影响的指令有些独特,因为它们是 一元 运算符,但与现有的一元运算符(如 notneg)不同,它们将单个操作数用作源和目标,它们具有不同的源和目标操作数,使它们表面上类似于大多数 2 输入指令。也许重命名器/调度器电路只是没有区分这些带有两个寄存器操作数的一元的特殊情况与绝大多数没有这种依赖关系的普通共享源/目标 2 输入指令。

事实上,对于popcnt 的情况,英特尔已经发布了各种勘误表,涵盖了错误的依赖问题,例如HSD146 用于Haswell Desktop,SKL029 用于Skylake,内容如下:

POPCNT 指令的执行时间可能比预期的要长

问题 使用 32 位或 64 位操作数执行 POPCNT 指令可能是 延迟到之前的非依赖指令执行完毕。

含义使用 POPCNT 指令的软件可能会遇到低于预期的性能。

解决方法未确定

我总是发现这个勘误表很不寻常,因为它并没有真正识别出任何类型的功能缺陷或不符合规范,而基本上所有其他勘误表都是这种情况。英特尔并没有真正记录 OoO 执行引擎的特定性能模型,并且多年来出现和消失的大量其他性能“陷阱”(许多比这个非常小的问题影响更大)没有没有在勘误表中记录。尽管如此,这也许提供了一些证据表明它可以被认为是一个错误。奇怪的是,勘误表从未扩展到包括 tzcntlzcnt,它们在引入时也存在同样的问题。


1tzcntlzcnt 只出现在 Haswell 中,但问题也存在于 Nehalem 中引入的 popcnt - 但是对于 Sandy 的错误依赖问题 perhaps only exists Bridge 或更高版本。

2在实践中,虽然没有在 ISA 文档中记录,因为英特尔手册中未定义全零输入的结果。然而,在这种情况下,大多数或所有 Intel 芯片都实现了保持目标寄存器不变的行为。
AMD does document 并保证 bsfbsr 的行为。

(但不幸的是这些指令比tzcnt/lzcnt慢 在 AMD 上(额外的 uops,请参阅 https://uops.info/),因此与其利用 bsf 行为,AMD CPU 使用 rep bsf 通常会更好,因此它会在知道的 CPU 上解码为 tzcnt那条指令,如果你有足够的空闲寄存器,test/cmov。但是bsr 给出了与lzcnt 不同的结果,即使对于非零输入,所以你可以考虑利用它。)

【讨论】:

  • 对于 AMD CPU,BSF/BSR 的 dst 未修改行为由 AMD ISA 参考文档记录。我预计未来的英特尔将在未来继续与 AMD 和当前的英特尔 CPU 兼容,几乎可以肯定的是,对于从其当前的 Sandybridge 系列(例如 Ice Lake)演变而来的 uarches。有可能他们会在一个全新的 uarch 中删除该执行单元的输出依赖项,特别是如果向后兼容不是那么重要(例如,如果他们再次执行类似 KNL 的操作)
  • @PeterCordes 您是否有指向 AMD ISA 中 BSF/BSR 文档的链接?我找不到它。
  • @Noah: developer.amd.com/resources/developer-guides-manualsamd x86 manual 在谷歌上的第一次点击。由于这不是特定于一个 uarch,因此在该页面上查找“AMD64 架构”手册,似乎 vol.3 是“通用”指令,而不是 simd 或 fp。相当于 Intel 的 vol.2 SDM 手册。 amd.com/system/files/TechDocs/24594.pdf#page=157 是 BSF 的条目,其中提到了 src=0 的情况。 (en.wikipedia.org/wiki/… 我认为即使对于 32 位操作数大小,整个 reg 也是真正未修改的。)
【解决方案2】:

按照@BrettHale 的建议,您有可能(如果奇怪的话)遇到了极端情况下的部分标志更新停滞。理论上,标志状态应该简单地重命名,因为下面的 add 会更新所有标志,但如果不是出于某种原因,它会引入循环携带的依赖关系,并且插入 xor 会破坏该依赖关系。

很难确定这是否是正在发生的事情,但随便一瞥就是最可能的解释;您可以通过将xor 替换为test 来测试假设(这也破坏了标志依赖性,但对寄存器依赖性没有影响)。

【讨论】:

  • 抱歉回复晚了。这是一个很好的理论,但不幸的是,测试证明了这一点。将xor改成test后,又变慢了。
  • @harold:这没什么不好的。你似乎已经排除了对齐,我们刚刚排除了部分标志依赖。 “当你消除了不可能的事情时,剩下的无论多么不可能,都必须是真相。”我们可能还没有完全消除其他所有内容,但看起来越来越像在您的处理器上实现的 xZCNT 在重命名时依赖于其输出寄存器。
  • 在得出这个结论之前,我还应该测试什么?
  • @StephenCanon:事实就是这样。这是英特尔 CPU 中的性能错误。现在知道了,gcc 尝试通过使用最近未使用的输出寄存器来解决它。谷歌应该能够找到一些命中。 IDK 被发现时;也许直到这次问答之后。
猜你喜欢
  • 2015-10-22
  • 2019-01-08
  • 2019-08-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多