【问题标题】:AMD64 Assembler: if- StatementAMD64 汇编器:if- 语句
【发布时间】:2014-03-26 09:44:41
【问题描述】:

我有两个 8 位寄存器,必须检查其中一个是否为 0。

我现在的解决方案是:

cmp $0, %r10b
je end
cmp $0, %r11b
je end

还有其他方法吗?

问候

【问题讨论】:

  • 是的,但由于在任何 x86-64 实现上的推测执行,它不太可能更有效。 cmp 更新所有标志,因此也没有部分标志寄存器停顿。

标签: if-statement assembly x86 x86-64


【解决方案1】:

此答案中的性能讨论适用于最近的 Intel CPU(Sandybridge、Haswell)。大多数情况下至少适用于 Pentium M,甚至更早的 P6(Pentium Pro / Pentium II)。请参阅http://agner.org/optimize/ 获取微架构文档。 AMD 上的性能考虑应该类似,除了它们不会像 Intel 将它们宏融合到单个 uop 中那样将测试和分支指令宏融合到单个宏操作中。

分支预测器存在于每个流水线设计中,但在 Haswell 之类的东西上比旧的前 Silvermont Atom 更重要。尽管如此,这部分还是相当普遍的。

对您的版本进行小调整:

test %r10b, %r10b   ; test is shorter than cmp with an immediate, but no faster
jz   end
test %r11b, %r11b
jz   end

可能只有一对test/jz 会在英特尔上进行宏熔断,因为它们可能会在同一个周期内同时击中解码器。此外,如果任一值是 ALU 运算的输出,它可能已经设置了零标志。所以安排你的代码,这样分支之一就不需要单独test


您可以保存一个分支(以额外的微指令为代价)。即使是未采用的分支的吞吐量也可能成为非常紧密的循环中的瓶颈。 Sandybridge 每 1-2 个周期只能维持 1 个分支。所以这个想法可能有助于:

test  %r10b, %r10b
setnz %r15b          # 1 if %r10b == 0, else 0
dec   %r15b          # 0 if %r10b == 0, else 0xFF
test  %r11b, %r15b
je    end

这又是一条指令(所有单 uop 指令都具有 1 个周期的延迟。)它在分支指令可以退出之前增加了更多的延迟(将错误预测惩罚增加了 3 个周期),但它可以提高性能:


为什么一个分支是好的:

如果a && b 是可预测的,但无法预测ab 中的哪一个实际上为零,这可以减少分支错误预测的数量。基准/性能计数器测试它,但是:据说程序员在猜测哪些分支可以在他们的代码中预测是出了名的糟糕。 CPU 的分支历史缓冲区大小有限,因此减少一个条目可能会有所帮助。


针对吞吐量进行了优化,延迟稍差:

如果延迟不重要,只考虑吞吐量(即错误预测很少发生):

# mov     %r10b, %al    # have the byte you want already stored in %r10b
imul    %r11b           # Intel: 3 cycle latency, 1/cycle throughput.
# ZF is undefined, not set according to the result, unfortunately
test    %ax, %ax        # No imm16, so no Intel length-changing-prefix stall for a 16bit insn
je    .end

总共 2 个微指令(test/je 可以进行宏熔断,即使在 AMD 上也是如此)。如果您需要保存 %al 的旧值,或者您无法免费获得 %al 中的某个值,那么这是一个额外的 mov。

如果你的寄存器的高字节被清零,你可能会获得速度:如果你使用字节操作将你的字节值放入寄存器,使用imul %r10d, %r11d 会创建一个部分寄存器停顿(或额外的 uop 来合并)。如果您编写了完整的 32 位寄存器(例如 movzx),那么您可以使用 imul 的 2 操作数形式,并测试 32 位结果。 (上面的 16 将全为零,这很好。)imul r8, r8 没有 2 操作数形式,无论如何您都需要完整的 16b 结果,因为它不会根据结果设置零标志。如果是这样,可能会有一个比较指令测试零和进位或溢出标志的正确组合。手册上说 ZF 在imul 之后是未定义的,所以不要依赖你当前的 CPU 发生了什么。这是你need the upper bytes to be zero的一种情况。

使test %ax, %ax 在 16 位寄存器上运行的操作数大小前缀不应导致 Intel 上的解码停止,因为它不会改变指令的 rest 长度。可怕的 LCP 停顿发生在 16 位立即数上,例如 test $0xffff, %ax,因此除非您只针对 AMD,否则请避免使用这些。

@Brett Hale 对 OP 的评论:如果您的分支指令依赖于设置标志的最后一条指令未修改的标志位。

【讨论】:

  • 嗯,IDK 我第一次尝试回答时的想法。感谢您发现我的愚蠢错误。现已修复。
  • 是的。 @10K,我看到了 2 个已删除的答案,它们产生了相同的初始错误 - 尝试保持 test/cmp/sub/jcc 等是很容易的。我总是对微优化持怀疑态度,因为它们是每个架构的滑动目标。我倾向于选择最直接的案例作为未来优化的最佳候选者。但是这个 +1 答案充满了有趣的细节。或许您可以通过处理器架构进一步限定它们?
  • 好点,我有时会忘记提及我主要关注的是最近英特尔(SnB 及更高版本)的时序和吞吐量。我补充了这一点和其他一些想法。是的,在 asm 中编写大量代码的工作量太大了。如果我已经在优化热循环并且它需要这个操作,我只会使用这样的技巧。不过,玩转性能创意很有趣。
  • 不要误会我的意思。我认为这种专业知识有足够的空间。 GMP 项目就是一个可以始终发挥你才能的例子。更不用说编译器技术了。除此之外,我喜欢人们仍然关心额外一两个周期的想法:)
【解决方案2】:

您可以先and,然后再做一个test?或者您也可以尝试按照@Peter Cordes 的建议进行乘法运算,但不要使用imul,而是使用lea

但我建议保留您当前的代码,只需使用 test 而不是 cmp,进行混淆处理。

实际上,由于test 执行and,只需在两个寄存器之间执行test,然后是jzjnz,甚至是cmov

【讨论】:

  • 对两个零的测试很棘手。 or 不保留该信息。 lea 也没有,因为它会移位和相加,但不会将两个寄存器相乘。该问题类似于尝试测试是否设置了某组位的 all。您通常必须屏蔽,然后检查结果 == 屏蔽。或者使用掩码反转和test,并检查ZF
  • 你是对的,但我的意思是“和”。如果 2 个寄存器中的一个为 0,则输出为 0,您只需对其进行测试即可。
  • 这也不是那么容易。如果是,我会使用test,而不是and,所以它不会破坏任何一个输入。 and / test 是按位的 &,而不是 C 的 &&1 & 2 为零,但两者均非零。它们的设置位不相交。
猜你喜欢
  • 1970-01-01
  • 2011-06-15
  • 2014-12-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-07-19
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多