阿里程序员工作小技巧 | 理解CPU分支预测,提高代码效率

简介:

vcg_VCG21400459891_RF_jpg_2400w_1i_90Q_jpeg

技术传播的价值,不仅仅体现在通过商业化产品和开源项目来缩短我们构建应用的路径,加速业务的上线速率,也会体现在优秀程序员在工作效率提升、产品性能优化和用户体验改善等小技巧方面的分享,以提高我们的工作能力。

从本期开始,我们将邀请来自阿里巴巴各个技术团队的程序员,涵盖中间件、前端、移动开发、大数据和人工智能等多个技术领域,分享他们在工作中的小技巧, 内容力求简短、实用和可操作。

第一期的分享嘉宾,是来自阿里巴巴中间件技术团队的程序员 - 断岭,他是阿里微服务开源项目 Dubbo 的项目组成员,也是Java线上诊断开源项目 Arthas 的负责人。

第一期:理解CPU分支预测,提高代码效率

一、基础概念:

  1. Dubbo: 是一款高性能、轻量级的开源Java RPC框架,提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现;
  2. ChannelEventRunnable: Dubbo 里所有网络事件的回调接口;
  3. JMH:即Java Microbenchmark Harness,是专门用于代码微基准测试的工具套件。在性能优化的过程中,可以使用JMH对优化的结果进行量化的分析。

二、需求缘起:

在Stack Overflow上有一个非常著名的问题:为什么处理有序数组要比非有序数组快?从问题的结论来看,是分支预测对代码运行效率的提升起到了非常重要的作用。

现今的CPU是都支持分支预测(branch prediction)和指令流水线(instruction pipeline),这俩的结合可以极大的提高CPU的工作效率,从而提高代码执行效率。但这仅适用于简单的if跳转,但对于Switch跳转,CPU则没有太好的解决办法,因为Switch本质上是据索引,是从地址数组里取地址再跳转。

三、思考和方案假设:

要提高代码执行效率,一个重要的实现原则就是尽量避免CPU把流水线清空,从Stack Overflow上的讨论结果来看,通过提高分支预测的成功率,是可以降低CPU对流水线清空的概率。那么,除了在硬件层面,是否可以考虑代码层面帮CPU把判断提前,来提高代码执行效率呢?

四、方案验证:

在Dubbo的ChannelEventRunnable里有一个Switch来判断channel state。当一个channel建立起来之后,超过99.9%的情况,它的state都是ChannelState.RECEIVED,我们可以考虑,把这个判断提前。

以下通过JMH来验证,把判断提前后是否就可以提高代码执行效率。

率。

public class TestBenchMarks {
public enum ChannelState {
    CONNECTED, DISCONNECTED, SENT, RECEIVED, CAUGHT    }

@State(Scope.Benchmark)
public static class ExecutionPlan {
    @Param({ "1000000" })
    public int size;
    public ChannelState[] states = null;

    @Setup
    public void setUp() {
        ChannelState[] values = ChannelState.values();
        states = new ChannelState[size];
        Random random = new Random(new Date().getTime());
        for (int i = 0; i < size; i++) {
            int nextInt = random.nextInt(1000000);
            if (nextInt > 100) {
                states[i] = ChannelState.RECEIVED;
            } else {
                states[i] = values[nextInt % values.length];
            }
        }
    }
}

@Fork(value = 5)
@Benchmark
@BenchmarkMode(Mode.Throughput)
public void benchSiwtch(ExecutionPlan plan, Blackhole bh) {
    int result = 0;
    for (int i = 0; i < plan.size; ++i) {
        switch (plan.states[i]) {
        case CONNECTED:
            result += ChannelState.CONNECTED.ordinal();
            break;
        case DISCONNECTED:
            result += ChannelState.DISCONNECTED.ordinal();
            break;
        case SENT:
            result += ChannelState.SENT.ordinal();
            break;
        case RECEIVED:
            result += ChannelState.RECEIVED.ordinal();
            break;
        case CAUGHT:
            result += ChannelState.CAUGHT.ordinal();
            break;
        }
    }
    bh.consume(result);
}

@Fork(value = 5)
@Benchmark
@BenchmarkMode(Mode.Throughput)
public void benchIfAndSwitch(ExecutionPlan plan, Blackhole bh) {
    int result = 0;
    for (int i = 0; i < plan.size; ++i) {
        ChannelState state = plan.states[i];
        if (state == ChannelState.RECEIVED) {
            result += ChannelState.RECEIVED.ordinal();
        } else {
            switch (state) {
            case CONNECTED:
                result += ChannelState.CONNECTED.ordinal();
                break;
            case SENT:
                result += ChannelState.SENT.ordinal();
                break;
            case DISCONNECTED:
                result += ChannelState.DISCONNECTED.ordinal();
                break;
            case CAUGHT:
                result += ChannelState.CAUGHT.ordinal();
                break;
            }
        }
    }
    bh.consume(result);
}}

验证说明:

  • benchSiwtch里是纯Switch判断
  • benchIfAndSwitch 里用一个if提前判断state是否ChannelState.RECEIVED

Benchmark结果是:

Result "io.github.hengyunabc.jmh.TestBenchMarks.benchSiwtch":
576.745 ±(99.9%) 6.806 ops/s [Average]
(min, avg, max) = (490.348, 576.745, 618.360), stdev = 20.066
CI (99.9%): [569.939, 583.550](assumes normal distribution)
Run complete. Total time: 00:06:48

Benchmark                         (size)   Mode  Cnt     Score    Error  Units
TestBenchMarks.benchIfAndSwitch  1000000  thrpt  100  1535.867 ± 61.212  ops/s
TestBenchMarks.benchSiwtch       1000000  thrpt  100   576.745 ±  6.806  ops/s

可以看到,提前if判断提高了近3倍的代码效率,这种技巧可以放在性能要求严格的地方。

五、总结:

  • Switch对于CPU来说难以做分支预测;
  • 某些Switch条件如果概率比较高,可以在代码层设置提前if判断,充分利用CPU的分支预测机制;
相关文章
|
5月前
|
Java Linux
linux中找到最耗CPU的那段Java代码
linux中找到最耗CPU的那段Java代码
|
2月前
|
弹性计算 大数据 测试技术
阿里服务器租用多少钱一年?阿里云服务器租用价格表(最新CPU/内存/带宽/磁盘收费标准)
阿里服务器租用多少钱一年?阿里云服务器租用价格表(最新CPU/内存/带宽/磁盘收费标准)。阿里云服务器的租用费用因实例类型、地域、配置等因素而有所不同,价格范围可以从几百元到几千元不等。2024年阿里云服务器租用费用价格表更新,云服务器ECS经济型e实例2核2G、3M固定带宽99元一年、ECS u1实例2核4G、5M固定带宽、80G ESSD Entry盘优惠价格199元一年,轻量应用服务器2核2G3M带宽轻量服务器一年61元、2核4G4M带宽轻量服务器一年165元12个月、2核4G服务器30元3个月,幻兽帕鲁4核16G和8核32G服务器配置,云服务器ECS可以选择经济型e实例、通用算力u1实
|
4月前
|
监控 数据可视化 Java
双CPU监控的目的以及主流编程语言实现代码示例
监控CPU使用率可以帮助检测系统瓶颈和性能问题,有助于及时识别并解决故障。
421 2
|
5月前
|
监控 调度 Python
电脑监控软件所含的CPU资源监控的代码(使用psutil库)
本文使用psutil库来获取CPU使用率、运行的进程、CPU温度、风扇速度和CPU核心的工作情况。这些信息可用于自定义电脑监控软件的CPU资源监控功能
484 1
|
5月前
|
存储 缓存 Java
这代码居然有差别?CPU友好的代码该这样写(4)
这代码居然有差别?CPU友好的代码该这样写
33 0
这代码居然有差别?CPU友好的代码该这样写(4)
|
5月前
|
存储 缓存 Java
这代码居然有差别?CPU友好的代码该这样写(3)
这代码居然有差别?CPU友好的代码该这样写
27 0
这代码居然有差别?CPU友好的代码该这样写(3)
|
5月前
|
缓存
这代码居然有差别?CPU友好的代码该这样写(2)
这代码居然有差别?CPU友好的代码该这样写
27 0
这代码居然有差别?CPU友好的代码该这样写(2)
|
5月前
|
存储 缓存 Java
这代码居然有差别?CPU友好的代码该这样写(1)
这代码居然有差别?CPU友好的代码该这样写
50 0
这代码居然有差别?CPU友好的代码该这样写(1)
|
5月前
|
缓存 安全 Java
从CPU的视角看 多线程代码为什么那么难写!
当我们提到多线程、并发的时候,我们就会回想起各种诡异的bug,比如各种线程安全问题甚至是应用崩溃,而且这些诡异的bug还很难复现。我们不禁发出了灵魂拷问 “为什么代码测试环境运行好好的,一上线就不行了?”。 为了解决线程安全的问题,我们的先辈们在编程语言中引入了各种各样新名词,就拿我们熟悉的Java为例,不仅java语言自带了synchronized、volatile、wait、notify… ,jdk中各种工具包也是层出不穷,就比如单一个Lock,就可以有很多种实现,甚至很多人都谈锁色变。
45 0
|
5月前
|
Java 测试技术 BI
一文告诉你CPU分支预测对性能影响有多大
CPU分支预测本身是为了提升流水线下避免流水线等待的手段,其实本质上是利用了局部性原理,因为局部性的存在,大多数情况下这个技术本身给性能带来的是正向的(要不然它今天也不会存在了),所以我们大多数情况下都不需要关注它的存在,还是放心大胆的写代码吧,不要因为我们这篇博客就把所有的if改成?:三目运算,可能对代码可读性的影响远大于性能提升的收益。再次强调下,我今天只是构造了一个极端的数据来验证其性能差异,因为局部性的存在大多数情况下分支预测都是对的。
55 0

相关实验场景

更多