如何度量一段代码的性能?选择性能最佳的实现方式?JMH帮你搞定!

什么是JMH

JMH(Java Microbenchmark Harness)是用于代码微基准测试的工具套件,主要是基于方法层面的基准测试,精度可以达到纳秒级。该工具是由 Oracle 内部实现 JIT 的大牛们编写的,他们应该比任何人都了解 JIT 以及 JVM 对于基准测试的影响。
官网: http://openjdk.java.net/projects/code-tools/jmh/
当你定位到热点方法,希望进一步优化方法性能的时候,就可以使用 JMH 对优化的结果进行量化的分析。
JMH 比较典型的应用场景如下:

  • 想准确地知道某个方法需要执行多长时间,以及执行时间和输入之间的相关性
  • 对比接口不同实现在给定条件下的吞吐量
  • 查看多少百分比的请求在多长时间内完成

下面我们以forEach和parallelStream两种方法遍历List为例子使用 JMH 做基准测试。

创建JMH测试工程

1. 创建Maven项目,添加依赖

<dependencies>
        <!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-core -->
        <dependency>
            <groupId>org.openjdk.jmh</groupId>
            <artifactId>jmh-core</artifactId>
            <version>1.21</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-generator-annprocess -->
        <dependency>
            <groupId>org.openjdk.jmh</groupId>
            <artifactId>jmh-generator-annprocess</artifactId>
            <version>1.21</version>
            <scope>test</scope>
        </dependency>
</dependencies>

2. idea安装JMH插件 JMH plugin v1.0.3
在这里插入图片描述

3. 由于用到了注解,打开运行程序注解配置

compiler -> Annotation Processors -> Enable Annotation Processing
在这里插入图片描述

4. 创建一个 JMH 测试类
创建测试类TestJMH 用来判断 forEach 和 parallelStream() 两种遍历方式哪个耗时更短,具体代码如下所示:

@BenchmarkMode(Mode.AverageTime)//
@Warmup(iterations = 3, time = 1)// 先预热3轮
@Measurement(iterations = 5, time = 5)// 进行5轮测试
@Threads(4)//4线程,同步迭代
@Fork(1)// Fork进行的数目
@State(value = Scope.Benchmark)// 每个Benchmark分配一个实例
@OutputTimeUnit(TimeUnit.MILLISECONDS)//结果所使用的时间单位:毫秒
public class TestJMH {
    @Param({"100","1000", "10000"}) // 定义3个参数,之后会分别对这3个参数进行测试
    private int n;
    static List<Integer> nums = new ArrayList<>();
    @Setup(Level.Trial) // 初始化方法,在全部Benchmark运行之前进行
    public void init() {
        Random r = new Random();
        for (int i= 0;i<n;i++)nums.add(1000000+r.nextInt(1000000));
    }

    @Benchmark
    public void foreach(){
        nums.forEach(v->isPrime(v));
    }

    @Benchmark
    public void parallel(){
        nums.parallelStream().forEach(JMH_01::isPrime);
    }

    static boolean isPrime(int num){
        for(int i=2; i<=num/2; i++) {
            if(num % i == 0) return false;
        }
        return true;
    }

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(TestJMH.class.getSimpleName())
                .result("result.json")
                .resultFormat(ResultFormatType.JSON).build();
        new Runner(opt).run();
    }
}

5. 运行测试类
运行过程中遇到下面的错误:

   ERROR: org.openjdk.jmh.runner.RunnerException: ERROR: Exception while trying to acquire the JMH lock (C:\WINDOWS\/jmh.lock): C:\WINDOWS\jmh.lock (拒绝访问。), exiting. Use -Djmh.ignoreLock=true to forcefully continue.
    at org.openjdk.jmh.runner.Runner.run(Runner.java:216)
    at org.openjdk.jmh.Main.main(Main.java:71)

从日志中可以看出这个错误是因为JMH运行需要访问系统的TMP目录,解决办法是:
打开RunConfiguration -> Environment Variables -> include system environment viables
在这里插入图片描述

6. 阅读测试报告

# testForEach方法参数为10000时的测试结果

# JMH version: 1.21
# VM version: JDK 1.8.0_181, Java HotSpot(TM) 64-Bit Server VM, 25.181-b13
# VM invoker: D:\Program Files\Java\jdk1.8.0_181\jre\bin\java.exe
# VM options: -javaagent:D:\Program Files\JetBrains\IntelliJ IDEA 2019.3.1\lib\idea_rt.jar=59425:D:\Program Files\JetBrains\IntelliJ IDEA 2019.3.1\bin -Dfile.encoding=UTF-8
# Warmup: 3 iterations, 1 s each
# Measurement: 5 iterations, 5 s each
# Timeout: 10 min per iteration
# Threads: 4 threads, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: com.jmh.TestJMH.foreach
# Parameters: (n = 10000)

# Run progress: 33.33% complete, ETA 00:02:00
# Fork: 1 of 1
# Warmup Iteration   1: 2246.782 ±(99.9%) 140.714 ms/op
# Warmup Iteration   2: 2186.002 ±(99.9%) 575.266 ms/op
# Warmup Iteration   3: 2482.109 ±(99.9%) 276.591 ms/op
Iteration   1: 2238.171 ±(99.9%) 288.741 ms/op
Iteration   2: 2395.052 ±(99.9%) 311.287 ms/op
Iteration   3: 2223.965 ±(99.9%) 133.162 ms/op
Iteration   4: 2246.373 ±(99.9%) 242.648 ms/op
Iteration   5: 2421.587 ±(99.9%) 498.462 ms/op

Result "com.jmh.TestJMH.foreach":
  2305.030 ±(99.9%) 366.175 ms/op [Average]
  (min, avg, max) = (2223.965, 2305.030, 2421.587), stdev = 95.095
  CI (99.9%): [1938.854, 2671.205] (assumes normal distribution)

# testParallel方法参数为10000时的测试结果

# JMH version: 1.21
# VM version: JDK 1.8.0_181, Java HotSpot(TM) 64-Bit Server VM, 25.181-b13
# VM invoker: D:\Program Files\Java\jdk1.8.0_181\jre\bin\java.exe
# VM options: -javaagent:D:\Program Files\JetBrains\IntelliJ IDEA 2019.3.1\lib\idea_rt.jar=59425:D:\Program Files\JetBrains\IntelliJ IDEA 2019.3.1\bin -Dfile.encoding=UTF-8
# Warmup: 3 iterations, 1 s each
# Measurement: 5 iterations, 5 s each
# Timeout: 10 min per iteration
# Threads: 4 threads, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: com.jmh.TestJMH.parallel
# Parameters: (n = 10000)

# Run progress: 83.33% complete, ETA 00:00:36
# Fork: 1 of 1
# Warmup Iteration   1: 1776.363 ±(99.9%) 2714.679 ms/op
# Warmup Iteration   2: 1599.224 ±(99.9%) 403.961 ms/op
# Warmup Iteration   3: 1843.330 ±(99.9%) 2049.332 ms/op
Iteration   1: 1652.597 ±(99.9%) 1265.922 ms/op
Iteration   2: 1663.783 ±(99.9%) 1086.725 ms/op
Iteration   3: 1645.228 ±(99.9%) 841.788 ms/op
Iteration   4: 1736.031 ±(99.9%) 2900.647 ms/op
Iteration   5: 1637.706 ±(99.9%) 1329.241 ms/op

Result "com.jmh.TestJMH.parallel":
  1667.069 ±(99.9%) 153.001 ms/op [Average]
  (min, avg, max) = (1637.706, 1667.069, 1736.031), stdev = 39.734
  CI (99.9%): [1514.068, 1820.070] (assumes normal distribution)
  
# 汇总结果
# Run complete. Total time: 00:02:48
Benchmark           (n)  Mode  Cnt     Score     Error  Units
TestJMH.foreach     100  avgt    5    15.150 ±   4.440  ms/op
TestJMH.foreach    1000  avgt    5   192.093 ±  41.756  ms/op
TestJMH.foreach   10000  avgt    5  2305.030 ± 366.175  ms/op
TestJMH.parallel    100  avgt    5    19.001 ±   1.687  ms/op
TestJMH.parallel   1000  avgt    5   196.770 ±  37.387  ms/op
TestJMH.parallel  10000  avgt    5  1667.069 ± 153.001  ms/op

Benchmark result is saved to result.json
Process finished with exit code 0

从最终汇总结果分析得出:在List集合size越大时 parallel 比 foreach 性能更好。

JMH可视化

为了更加直观的查看测试结果,我们可以借助可视化工具JMH Visual chart 进行可视化
GitHub地址:https://github.com/Sayi/jmh-visual-chart
导入测试生成的结果数据result.json
在这里插入图片描述

JMH中的基本概念

为了能够更好地使用 JMH 的各项功能,下面对 JMH 的基本概念进行讲解:

@BenchmarkMode
用来配置 Mode 选项,可用于类或者方法上,这个注解的 value 是一个数组,可以把几种 Mode 集合在一起执行,如:@BenchmarkMode({Mode.SampleTime, Mode.AverageTime}),还可以设置为 Mode.All,即全部执行一遍。

  1. Throughput:整体吞吐量,每秒执行了多少次调用,单位为 ops/time
  2. AverageTime:用的平均时间,每次操作的平均时间,单位为 time/op
  3. SampleTime:随机取样,最后输出取样结果的分布
  4. SingleShotTime:只运行一次,往往同时把 Warmup 次数设为 0,用于测试冷启动时的性能
  5. All:上面的所有模式都执行一次

@State
通过 State 可以指定一个对象的作用范围,JMH 根据 scope 来进行实例化和共享操作。@State 可以被继承使用,如果父类定义了该注解,子类则无需定义。由于 JMH 允许多线程同时执行测试,不同的选项含义如下:

  1. Scope.Benchmark:所有测试线程共享一个实例,测试有状态实例在多线程共享下的性能
  2. Scope.Group:同一个线程在同一个 group 里共享实例
  3. Scope.Thread:默认的 State,每个测试线程分配一个实例

@OutputTimeUnit
为统计结果的时间单位,可用于类或者方法注解,最小可以精确到NANOSECONDS(纳秒)级别

@Warmup
预热所需要配置的一些基本测试参数,可用于类或者方法上。一般前几次进行程序测试的时候都会比较慢,所以要让程序进行几轮预热,保证测试的准确性。参数如下所示:

  1. iterations:预热的次数
  2. time:每次预热的时间
  3. timeUnit:时间的单位,默认秒
  4. batchSize:批处理大小,每次操作调用几次方法

为什么需要预热?
因为 JVM 的 JIT 机制的存在,如果某个函数被调用多次之后,JVM 会尝试将其编译为机器码,从而提高执行速度,所以为了让 benchmark 的结果更加接近真实情况就需要进行预热。

@Measurement
实际调用方法的次数,可用于类或者方法上,参数和 @Warmup 相同。

@Threads
每个进程中的测试线程,可用于类或者方法上。

@Fork
进行 fork 的次数,可用于类或者方法上。如果 fork 数是 2 的话,则 JMH 会 fork 出两个进程来进行测试。
@Param
指定某项参数的多种情况,特别适合用来测试一个函数在不同的参数输入的情况下的性能,只能作用在字段上,使用该注解必须定义 @State 注解。

总结

本文主要从概念和简单样例介绍了性能基准测试工具 JMH,它可以通过对某个方法或者类进行精准测试来规避由 JVM 中的 JIT 或者其他优化对性能测试造成的影响。
只需要将待测的业务逻辑用 @Benchmark 注解标识,就可以让 JMH 的注解处理器自动生成真正的性能测试代码,以及相应的性能测试结果,个人觉得JMH在性能测试场景下是一个非常简单实用的工具。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值