【问题标题】:Small branches in modern CPUs现代 CPU 中的小分支
【发布时间】:2019-07-24 14:56:21
【问题描述】:

像 Kaby Lake 这样的现代 CPU 如何处理小分支? (在下面的代码中,它是到标签 LBB1_67 的跳转)。据我所知,分支不会有害,因为跳转不如解码窗口大小的 16 字节块大小。

或者是否有可能由于某些宏操作融合,分支将被完全省略?

        sbb     rdx, qword ptr [rbx - 8]
        setb    r8b
        setl    r9b
        mov     rdi, qword ptr [rbx]
        mov     rsi, qword ptr [rbx + 8]
        vmovdqu xmm0, xmmword ptr [rbx + 16]
        cmp     cl, 18
        je      .LBB1_67
        mov     r9d, r8d
.LBB1_67:                               #   in Loop: Header=BB1_63 Depth=1
        vpcmpeqb        xmm0, xmm0, xmmword ptr [rbx - 16]
        vpmovmskb       ecx, xmm0
        cmp     ecx, 65535
        sete    cl
        cmp     rdi, qword ptr [rbx - 32]
        sbb     rsi, qword ptr [rbx - 24]
        setb    dl
        and     dl, cl
        or      dl, r9b

【问题讨论】:

    标签: performance x86-64 cpu-architecture avx branch-prediction


    【解决方案1】:

    在任何 x86 CPU 中都没有短分支距离的特殊情况。即使是无条件的jmp 到下一条指令(架构上是 nop)也需要正确的分支预测才能有效地处理;如果您连续放置足够多的条目,您的 BTB 条目就会用完,性能就会跌落悬崖。 Slow jmp-instruction

    获取/解码只是一个小问题;是的,同一缓存行中的一个非常短的分支仍然会在 L1i 中命中,并且可能会在 uop 缓存中命中。但解码器不太可能对预测的向前跳转进行特殊处理,并利用从包含分支和目标的块中找到的预解码指令边界。

    当指令被解码为微指令并输入前端时,寄存器值不可用;这些仅在乱序执行后端可用。

    主要问题是当.LBB1_67:之后的指令执行时,架构状态会根据分支是否被采取而不同。 微架构状态也是如此(RAT = 寄存器分配表)。

    要么:

    • r9 取决于 sbb/setl 结果(mov r9d, r8d 没有运行)
    • r9 取决于 sbb/setb 结果(mov r9d, r8d 确实运行了)

    条件分支在计算机体系结构术语中称为“控制依赖项”。分支预测 + 推测执行避免将控制依赖关系转变为数据依赖关系。如果预测未采用 je,则 setl 结果(r9 的旧值)将被 mov 覆盖,并且不再可用。

    在检测到je 中的错误预测(实际上应该采取)之后,无法从这种情况中恢复过来,尤其是在一般情况下。当前的 x86 CPU 不会尝试寻找退出路径以重新加入已采用的路径或弄清楚它的作用。

    如果cl 很长时间没有准备好,所以很长时间没有发现错误预测,or dl, r9b 之后的许多指令可能使用错误的输入执行。在一般情况下,可靠+有效恢复的唯一方法是丢弃所有根据“错误”路径的指令完成的工作。例如,检测到vpcmpeqb xmm0, [rbx - 16] 仍然以任何一种方式运行是很困难的,而不是寻找。 (现代英特尔,自 Sandybridge 以来,有一个分支顺序缓冲区 (BOB),它可以对分支上的 RAT 进行快照,一旦执行检测到分支未命中,就可以有效回滚到它,同时仍然允许在早期执行无序执行 指令在回滚期间继续。在此之前,分支未命中必须回滚到退休状态。)


    用于某些非 x86 ISA(例如我认为的 PowerPC)的一些 CPU 已经尝试将恰好跳过 1 条指令的前向分支转换为预测(数据依赖),而不是推测过去。例如Dynamic Hammock Predication for Non-predicated Instruction Set Architectures 讨论了这个想法,甚至决定是否基于每个分支进行谓词。如果你的分支预测历史表明这个分支预测不好,那么预测它可能是好的。 (Hammock 分支是在一条或几条指令上向前跳转的分支。在具有固定宽度指令字的 ISA(如 RISC)上检测恰好 1 条指令的情况是微不足道的,但在 x86 上却很难。)

    在这种情况下,x86 有一个cmovcc 指令,这是一个 ALU 选择操作,它根据标志条件产生两个输入之一。 cmove r9d, r8d 代替 cmp/je 可以避免分支错误预测,但代价是为使用 r9d 的指令引入了对 clr8d 的数据依赖。英特尔 CPU 不会尝试为您执行此操作。

    (在 Broadwell 和后来的 Intel 上,cmov 仅为 1 uop,低于 2. cmp/jcc 为 1 uop,而mov 本身也是 1 uop,因此在未采用的情况下,cmov 也是前端的 uops 更少。而在采用的情况下,即使预测正确,采用的分支也会在管道中引入气泡,这取决于代码的吞吐量有多高:阶段之间的队列是否可以吸收它。)

    请参阅gcc optimization flag -O3 makes code slower than -O2 以了解 CMOV 比分支慢的情况,因为引入数据依赖关系不好。

    【讨论】:

    • 根据Henry关于返回地址预测的博文(blog.stuffedcow.net/2018/04/ras-microbenchmarks),BOB结构从SnB开始就存在了。
    • 对 Haswell 的快速测试表明,条件跳转对性能的影响小于 0.0001%。
    • @HadiBrais:不过,我想你会发现对于一个距离更远的容易预测的分支,你会发现同样的事情。
    • @HadiBrais - 对,分支的影响很容易为零,但这不是一般性的陈述:它也很容易变得更多,具体取决于周围的代码。采用分支是前端的一个已知瓶颈,很容易找到与重新排列的未采用版本相比,采用分支使代码速度降低 100% 或更多的示例。
    • @PeterCordes:感谢您的提示!不知道杀手鲍勃。是否有经验法则可以知道何时不需要预测分支(例如,寄存器 rX 准备好 C 在 cmp/je 之前循环)?
    猜你喜欢
    • 2015-09-17
    • 2012-09-03
    • 1970-01-01
    • 2012-07-11
    • 1970-01-01
    • 2013-05-07
    • 2014-02-26
    • 1970-01-01
    • 2012-04-06
    相关资源
    最近更新 更多