此答案中的性能讨论适用于最近的 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 是可预测的,但无法预测a 或b 中的哪一个实际上为零,这可以减少分支错误预测的数量。基准/性能计数器测试它,但是:据说程序员在猜测哪些分支可以在他们的代码中预测是出了名的糟糕。 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 的评论:如果您的分支指令依赖于设置标志的最后一条指令未修改的标志位。