当前位置: 首页 > article >正文

JVM常见概念之条件移动

问题

当我们有分支频率数据时,有什么有趣的技巧可以做吗?什么是条件移动?

基础知识

如果您需要在来自一个分支的两个结果之间进行选择,那么您可以在 ISA 级别做两件不同的事情。

首先,你可以创建一个分支:

    # %r = (%rCond == 1) ? $v1 : $v2
    cmp %rCond, $1
    jne A
    mov %r, $v1
    jmp E
 A: mov %r, $v2
 E:

其次,您可以执行依赖于比较结果的预测指令 。在 x86 中,这采用条件移动 (CMOV) 的形式,当选定条件成立时执行操作:

# %r = (%rCond == 1) ? $v1 : $v2
  mov %r, $v1      ; put $v1 to %r
  cmp %rCond, ...
  cmovne %r, $v2   ; put $v2 to %r if condition is false

执行条件移动的优点是它有时会生成更紧凑的代码,就像在这个例子中一样,并且它不会受到可能的分支预测错误惩罚。缺点是它需要在选择返回哪一边之前计算两边,这可能会花费过多的 CPU 周期,增加寄存器压力等。在分支情况下,我们可以选择在检查条件后不计算内容。预测良好的分支将优于条件移动。

因此,是否执行条件移动的选择在很大程度上取决于其成本预测。这就是分支分析可以帮助我们的地方:它可以说出哪些分支可能没有被完美预测,因此适合 CMOV 替换。当然, 实际成本模型还包括我们正在处理的参数类型、两个计算分支的相对深度等。

实验

源码-用例1

@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(1)
@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class BranchFrequency {
    @Benchmark
    public void fair() {
        doCall(true);
        doCall(false);
    }

    @CompilerControl(CompilerControl.Mode.DONT_INLINE)
    public int doCall(boolean condition) {
        if (condition) {
            return 1;
        } else {
            return 2;
        }
    }
}

执行结果

我们在每次调用时都会在分支之间进行切换,这意味着它的运行时配置文件在它们之间大约是 50%-50%。如果我们通过提供 -XX:ConditionalMoveLimit=0 来限制条件移动替换,那么我们就可以清楚地看到替换的发生。

# doCall, out of box variant
  4.36%  ...4ac: mov    $0x1,%r11d         ; move $1 -> %r11
  3.24%  ...4b2: mov    $0x2,%eax          ; move $2 -> %res
  8.46%  ...4b7: test   %edx,%edx          ; test boolean
  0.02%  ...4b9: cmovne %r11d,%eax         ; if false, move %r11 -> %res
  7.88%  ...4bd: add    $0x10,%rsp         ; exit the method
  8.12%  ...4c1: pop    %rbp
 18.60%  ...4c2: cmp    0x340(%r15),%rsp
         ...4c9: ja     ...4d0
  0.14%  ...4cf: retq

# doCall, CMOV conversion inhibited
  6.48%    ...cac: test   %edx,%edx         ; test boolean
        ╭  ...cae: je     ...cc8
        │                                   ; if true...
        │  ...cb0: mov    $0x1,%eax         ; move $1 -> %res
  7.41% │↗ ...cb5: add    $0x10,%rsp        ; exit the method
  0.02% ││ ...cb9: pop    %rbp
 27.43% ││ ...cba: cmp    0x340(%r15),%rsp
        ││ ...cc1: ja     ...ccf
  3.28% ││ ...cc7: retq
        ││                                  ; if false...
  7.04% ↘│ ...cc8: mov    $0x2,%eax         ; move $2 -> %res
  0.02%  ╰ ...ccd: jmp    ...cb5            ; jump back

在此示例中,CMOV 版本的表现稍好一些:

Benchmark                              Mode  Cnt   Score    Error  Units

# Branches
BranchFrequency.fair                   avgt   25   5.422 ±  0.026  ns/op
BranchFrequency.fair:L1-dcache-loads   avgt    5  12.078 ±  0.226   #/op
BranchFrequency.fair:L1-dcache-stores  avgt    5   5.037 ±  0.120   #/op
BranchFrequency.fair:branch-misses     avgt    5   0.001 ±  0.003   #/op
BranchFrequency.fair:branches          avgt    5  10.037 ±  0.216   #/op
BranchFrequency.fair:cycles            avgt    5  14.659 ±  0.285   #/op
BranchFrequency.fair:instructions      avgt    5  35.184 ±  0.559   #/op

# CMOVs
BranchFrequency.fair                   avgt   25   4.799 ±  0.094  ns/op
BranchFrequency.fair:L1-dcache-loads   avgt    5  12.014 ±  0.329   #/op
BranchFrequency.fair:L1-dcache-stores  avgt    5   5.005 ±  0.167   #/op
BranchFrequency.fair:branch-misses     avgt    510⁻⁴            #/op
BranchFrequency.fair:branches          avgt    5   7.054 ±  0.118   #/op
BranchFrequency.fair:cycles            avgt    5  12.964 ±  1.451   #/op
BranchFrequency.fair:instructions      avgt    5  36.285 ±  0.713   #/op

您可能认为这是因为 CMOV 没有分支预测失误惩罚,但这种解释与计数器不一致。请注意,在两种情况下,“分支失误”几乎为零。这是因为硬件分支预测器实际上可以记住一个短暂的分支历史,而这种反复出现的分支对它们来说没有任何问题。性能差异的实际原因是分支情况下的跳跃:我们在关键路径上有一条额外的控制流指令。

源码-用例2

@Warmup(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS)
@Measurement(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS)
@Fork(1)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
public class AdjustableBranchFreq {

    @Param("50")
    int percent;

    boolean[] arr;

    @Setup(Level.Iteration)
    public void setup() {
        final int SIZE = 100_000;
        final int Q = 1_000_000;
        final int THRESH = percent * Q / 100;
        arr = new boolean[SIZE];
        ThreadLocalRandom current = ThreadLocalRandom.current();
        for (int c = 0; c < SIZE; c++) {
            arr[c] = current.nextInt(Q) < THRESH;
        }

        // Avoid uncommon traps on both branches.
        doCall(true);
        doCall(false);
    }

    @Benchmark
    public void test() {
        for (boolean cond : arr) {
            doCall(cond);
        }
    }

    @CompilerControl(CompilerControl.Mode.DONT_INLINE)
    public int doCall(boolean condition) {
        if (condition) {
            return 1;
        } else {
            return 2;
        }
    }
}

执行结果

使用不同的 percent 值和 -prof perfnorm JMH 分析器运行它将产生以下结果:
在这里插入图片描述依据上图,你可以清楚地看到几件事:

  • 每个测试的分支数约为 5,而 CMOV 转换将其降至 4。这与之前的反汇编转储相关:我们将测试中的一个分支转换为 CMOV。另外 4 个分支来自测试基础设施本身。
  • 如果没有 CMOV,分支测试性能会受到影响,在 50% 的分支概率下会变得最差。这个峰值反映了硬件分支预测器几乎完全混乱,因为它每次操作都会遇到大约 0.5 次分支失误。这意味着分支预测器并不是一直猜错(这太荒谬了!),而只是一半的时间猜错。我推测基于历史的预测器会放弃,让静态预测器选择最近的分支,而我们只选择了一半的时间。
  • 使用 CMOV 后,我们可以看到操作时间几乎持平 。该图表明 CMOV 成本模型对于此测试来说可能过于保守,并且切换得有点晚。这并不一定意味着它有错误,因为其他情况的表现很可能会有所不同。尽管如此,当进行 CMOV 转换时,对分支情况的改进是巨大的。
  • 您可能会注意到,当分支预测准确率为 >97% 时,分支变体会低于 CMOV 中间平均值。当然,这又是测试、硬件、虚拟机特有的事情。

总结

分支分析允许在执行概率敏感指令选择时做出或多或少明智的选择。条件移动替换通常使用分支频率信息来驱动替换。这再次强调了使用与真实数据类似的数据来预热 JIT 编译代码的必要性,以便编译器能够针对特定情况进行有效优化。


http://www.kler.cn/a/600028.html

相关文章:

  • git安装教程
  • Java 基础入门代码示例解析
  • idea底部图标不小心关了,怎么重新打开?
  • Leetcode—15. 三数之和(哈希表—基础算法)
  • Java多线程与高并发专题——使用 Future 有哪些注意点?Future 产生新的线程了吗?
  • 内网渗透-隧道通信
  • Python技术栈与数据可视化创意实践详解(三)
  • 【进阶编程】跨平台的 UI 框架
  • JVM 02
  • STM32G030移植RT-Thread
  • 学一个前端 UI 框架,要学些什么内容?
  • 当人类关系重构:从“相互需要”到“鹅卵石化”——生成式人工智能(GAI)认证的角色与影响
  • 探索AI的无限可能,体验智能对话的未来,大模型 API 演示
  • linux ptrace 图文详解(三) PTRACE_ATTACH 跟踪程序
  • Edge浏览器如何默认启动某个工作区 / 为工作区添加快捷方式
  • docker 容器 php环境中安装gd 、mysql 等扩展
  • 数据库原理及应用mysql版陈业斌实验一
  • 【Docker系列二】 Docker 镜像
  • Spring-Mybatis框架常见面试题
  • Java面试第十三山!《设计模式》