Java开源工具库使用之性能测试JMH

41 篇文章 2 订阅
31 篇文章 0 订阅

前言

JMH(Java Microbenchmark Harness),是 OpenJDK 团队开发的一款基准测试工具,一般用于代码的性能比较和调优,精度甚至可以达到纳秒级别,适用于 java 以及其他基于 JVM 的语言。

github地址:https://github.com/openjdk/jmh

官方使用例子:https://github.com/openjdk/jmh/tree/master/jmh-samples/src/main/java/org/openjdk/jmh/samples

pom 依赖:

<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-core</artifactId>
    <version>1.35</version>
</dependency>
<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-generator-annprocess</artifactId>
    <version>1.35</version>
</dependency>

一、配置参数

1.1 注解列表

注解名作用域作用
AuxCountersTYPE辅助计数器,可以统计 @State 修饰的对象中的 public 属性被执行的情况。实验性API,将来可能删除
BenchmarkMETHOD标记为基准测试方法,和 junit @Test 类似
BenchmarkModeTYPE,METHOD指明了基准测试的模式, 模式可以任意组合,详细见下方
CompilerControlTYPE,METHOD,CONSTRUCTOR编译控制选项,是否使用编译优化
ForkTYPE,METHODfork出 jvm 子进程进行测试,一般设置为1
GroupMETHOD控制多线程组
GroupThreadsMETHOD设置参与组的线程数量
MeasurementTYPE,METHOD设置默认测量参数
OperationsPerInvocationTYPE,METHOD设置单个 benchmark 方法 op 个数(默认1个benchmark一个op)
OutputTimeUnitTYPE.METHOD指定输出的时间单位,可以传入 java.util.concurrent.TimeUnit 中的时间单位,最小可以到纳秒级别
ParamFIELD允许使用一个 Benchmark 方法跑多组数据,特别适合测量方法性能和参数取值的关系
SetupMETHOD用于基准测试前的初始化动作,可通过参数 level 确定粒度, 具体见下方
StateTYPE声明某个类是一个“状态”,然后接受一个 Scope 参数用来表示该状态的共享范围, 当使用 @Setup 的时候,必须在类上加这个参数,不然会提示无法运行。参数设置见下方
TearDownMETHOD用于基准测试后的动作
ThreadsTYPE,METHOD设置线程数
TimeoutTYPE,METHOD设置默认超时参数,java.util.concurrent.TimeUnit
WarmupTYPE.METHOD设置默认预热参数,详细见下方
  • AuxCounters

    • Type.EVENTS: 统计发生的次数
    • Type.OPERATIONS:按指定的格式统计,如按吞吐量统计
  • BenchmarkMode

    • Mode.Throughput :吞吐量,单位时间内执行的次数,默认值
    • Mode.AverageTime:平均时间,一次执行需要的单位时间,其实是吞吐量的倒数
    • Mode.SampleTime:是基于采样的执行时间,采样频率由JMH自动控制,同时结果中也会统计出p90、p95的时间
    • Mode.SingleShotTime:单次执行时间,只执行一次,可用于冷启动的测试
  • CompilerControl

    • Mode.BREAK:在生成的编译代码插入断点

    • Mode.PRINT:打印方法及配置文件

    • Mode.EXCLUDE:从编译中排除该方法

    • Mode.INLINE:强制使用内联

    • Mode.DONT_INLINE:强制跳过内联

    • Mode.COMPILE_ONLY:仅仅编译方法,其它啥都不干

  • Measurement

    • iterations:测量迭代次数,不是方法执行次数
    • time:每次迭代时间
    • timeUnit:时间单位
    • batchSize:一次迭代方法需要执行次数
  • Setup/TearDown

    • Level.Trial:Benchmark级别
    • Level.Iteration:执行迭代级别
    • Level.Invocation:每次方法调用级别
  • State

    • Scope.Thread:作用域为线程
    • Scope.Benchmark:作用域为本次JMH测试,线程共享
    • Scope.Group:作用域为组
  • WarmUp

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

    • iterations:预热迭代次数
    • time:每次迭代时间
    • timeUnit:时间单位
    • batchSize:一次迭代方法需要执行次数

1.2 非注解配置

除了上述使用注解进行配置,还有一些参数可以通过 OptionsBuilder 这个类进行配置

  • include 配置参与基准测试的类,参数是类的简单名称,不包含包名
  • exclude 排除的方法名,include 会默认导入所有 @Benchmark public 方法
  • addProfiler 添加分析器,能够得到更多关于 jvm 的信息。jmh 自身提供了很多分析器:如 GCProfiler, StackProfiler, ClassloaderProfiler 等等
  • detectJvmArgs 从父jvm检测参数,会覆盖jvmArgs
  • jvmArgs fork jvm 参数
  • shouldDoGC 在mesurement 迭代之间是否GC
  • verbosity 控制输出信息的级别

1.3 概念

  • JMH使用OPS来表示吞吐量,OPS,Opeartion Per Second,是衡量性能的重要指标,指得是每秒操作量。数值越大,性能越好。类似的概念还有TPS,表示每秒的事务完成量,QPS,每秒的查询量。
  • 如果对每次执行时间进行升序排序,取出总数的99%的最大执行时间作为 p99 的值,p99 通常是衡量系统性能重要指标,表示99%的请求的响应时间不超过某个值,类似的还有p95,p90, p999
  • 测试时间,测试时间 = (测试方法数量) * (warmup迭代次数 * 时间 + measurement迭代次数 * 时间) * (@Param参数个数的乘积) * (forks)

二、简单例子

2.1 throught(吞吐量)

先用一个最简单的例子做测试, 计算 testThrought 这个方法1s执行多少次,就是计算吞吐量

package com.aabond.demo.jmh;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

public class JmhDemo {
    
    @Benchmark
    public void testThrought() throws InterruptedException {
        Thread.sleep(1000);
    }

    public static void main(String[] args) throws RunnerException {

        Options opt = new OptionsBuilder()
                .include(JmhDemo.class.getSimpleName())
                .forks(1)
                .build();

        new Runner(opt).run();
    }

}
Benchmark              Mode  Cnt  Score   Error  Units
JmhDemo.testThrought  thrpt    5  0.991 ± 0.004  ops/s

代码中,可以看出 testThrought 肯定会耗费1s左右的时间,结果正如所预料的一样。

上述代码只是用了最基本的默认配置,更多参数配置可以通过注解和代码来控制。

  • 默认的预热和迭代次数都是5,可以用@Warmup和@Measurement来自定义
  • 默认输出时间单位是秒,也可以用@OutputTimeUnit 实现显示其它单位
  • 默认基准测试是输出吞吐量,可以用@BenchmarkMode 设置平均时间,这两者互为倒数
@Warmup(iterations = 3)
@Measurement(iterations = 6)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@BenchmarkMode(value = Mode.AverageTime)
public class JmhDemo {
    
    @Benchmark
    public void testThrought() throws InterruptedException {
        Thread.sleep(1000);
    }

    public static void main(String[] args) throws RunnerException {

        Options opt = new OptionsBuilder()
                .include(JmhDemo.class.getSimpleName())
                .forks(1)
                .build();

        new Runner(opt).run();
    }

}
Benchmark             Mode  Cnt     Score   Error  Units
JmhDemo.testThrought  avgt    6  1009.491 ± 4.268  ms/op

2.2 ArrayList vs Set

ArrayList和 Set 的查找时间复杂度分别是 O ( n ) O(n) O(n) O ( 1 ) O(1) O(1),利用 jmh 测试一下差距

使用 datafaker 准备100,1000,10000个中文姓名字符串, 再用100个随机中文姓名字符串进行查找,用这种方法进行测试

@Warmup(iterations = 3)
@Measurement(iterations = 6)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
@BenchmarkMode(value = Mode.AverageTime)
public class JmhDemo {
    
    List<String> names = new ArrayList<>();
    Set<String> namesSet = new HashSet<>();
    List<String> finds = new ArrayList<>();
    
    @Param({"100", "1000", "10000"})
    int originLen;
    
    @Setup
    public void setUp() {
        Faker faker = new Faker(Locale.CHINA);
        names = faker.collection(() -> faker.name().name()).len(originLen).generate();
        namesSet = new HashSet<>(names);
        finds =  faker.collection(() -> faker.name().name()).len(100).generate();
    }

    @Benchmark
    @OperationsPerInvocation(100)
    public boolean listFind() {
        for (String name: finds) {
            boolean b = names.contains(name);
        }
        return true;
    }

    @Benchmark
    @OperationsPerInvocation(100)
    public boolean setFind() {
        for (String name: finds) {
            boolean b = namesSet.contains(name);
        }
        return true;
    }

    public static void main(String[] args) throws RunnerException {

        Options opt = new OptionsBuilder()
                .include(JmhDemo.class.getSimpleName())
                .forks(1)
                .build();

        new Runner(opt).run();
    }

}
Benchmark         (originLen)  Mode  Cnt   Score    Error  Units
JmhDemo.listFind          100  avgt    610⁻⁴           ms/op
JmhDemo.listFind         1000  avgt    6   0.004 ±  0.001  ms/op
JmhDemo.listFind        10000  avgt    6   0.063 ±  0.006  ms/op
JmhDemo.setFind           100  avgt    610⁻⁵           ms/op
JmhDemo.setFind          1000  avgt    610⁻⁵           ms/op
JmhDemo.setFind         10000  avgt    610⁻⁵           ms/op

从结果可以看出,List 随着数据量的增大查找的速度逐渐变慢,数据量从 1 0 2 10^2 102-> 1 0 3 10^3 103-> 1 0 4 10^4 104, 查找耗费时间 1 0 − 4 10^{-4} 104-> 1 0 − 3 10^{-3} 103-> 1 0 − 2 10^{-2} 102

而 set 一直保持不变,保持在 1 0 − 5 10^{-5} 105

2.3 StringBuilder vs StringBuffer

StringBuilder 和 StringBuffer 都可以用来拼接字符串。一个是线程不安全的,而另一个是线程安全的。实际用 StringBuilder 用的比较多,想知道这两者的差异,用 jmh 来比较下速度。

@Warmup(iterations = 3, time = 5)
@Measurement(iterations = 6, time = 5)
@Fork(1)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
@BenchmarkMode(value = Mode.AverageTime)
public class JmhDemo {
    List<String> names = new ArrayList<>();

    @Param({"1000", "100000", "10000000"})
    int originLen;
    
    @Setup
    public void setUp() {
        Faker faker = new Faker(Locale.CHINA);
        names = faker.collection(() -> faker.name().name()).len(originLen).generate();
    }


    @Benchmark
    public void stringBufferAppend(Blackhole bh) {
        StringBuffer sb = new StringBuffer();
        for (String name: names) {
            sb.append(name);
        }
        bh.consume(sb);
    }

    @Benchmark
    public void stringBuilderAppend(Blackhole bh) {
        StringBuilder sb = new StringBuilder();
        for (String name: names) {
            sb.append(name);
        }
        bh.consume(sb);
    }

    public static void main(String[] args) throws RunnerException {

        Options opt = new OptionsBuilder()
                .include(JmhDemo.class.getSimpleName())
                .build();

        new Runner(opt).run();
    }

}
Benchmark                    (originLen)  Mode  Cnt    Score   Error  Units
JmhDemo.stringBufferAppend          1000  avgt    6    0.016 ± 0.002  ms/op
JmhDemo.stringBufferAppend        100000  avgt    6    1.635 ± 0.363  ms/op
JmhDemo.stringBufferAppend      10000000  avgt    6  162.367 ± 3.866  ms/op
JmhDemo.stringBuilderAppend         1000  avgt    6    0.015 ± 0.003  ms/op
JmhDemo.stringBuilderAppend       100000  avgt    6    1.701 ± 1.013  ms/op
JmhDemo.stringBuilderAppend     10000000  avgt    6  150.966 ± 4.673  ms/op

从结果看,StringBuffer和StringBuilder 拼接字符串的效率相差不大

2.4 Stream vs parallelStream vs for

@Warmup(iterations = 3, time = 5)
@Measurement(iterations = 6, time = 5)
@Fork(1)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
@BenchmarkMode(value = Mode.AverageTime)
public class JmhDemo {

    @Param({"100000", "1000000", "10000000", "100000000"})
    int originLen;

    @Benchmark
    public void streamGenerate(Blackhole bh) {
        int[] array = IntStream.range(0, originLen).toArray();
        bh.consume(array);
    }

    @Benchmark
    public void streamParallelGenerate(Blackhole bh) {
        int[] array = IntStream.range(0, originLen).parallel().toArray();
        bh.consume(array);
    }

    @Benchmark
    public void forGenerate(Blackhole bh) {
        int [] array = new int[originLen];
        for (int i = 0; i < originLen; i++) {
            array[i] = i;
        }
        bh.consume(array);
    }

    public static void main(String[] args) throws RunnerException {

        Options opt = new OptionsBuilder()
                .include(JmhDemo.class.getSimpleName())
                .result("E:\\list.json")
                .resultFormat(ResultFormatType.JSON)
                .build();

        new Runner(opt).run();
    }

}
Benchmark                       (originLen)  Mode  Cnt    Score     Error  Units
JmhDemo.forGenerate                  100000  avgt    6    0.068 ±   0.015  ms/op
JmhDemo.forGenerate                 1000000  avgt    6    0.653 ±   0.105  ms/op
JmhDemo.forGenerate                10000000  avgt    6    6.581 ±   1.462  ms/op
JmhDemo.forGenerate               100000000  avgt    6   85.288 ± 104.922  ms/op
JmhDemo.streamGenerate               100000  avgt    6    0.098 ±   0.013  ms/op
JmhDemo.streamGenerate              1000000  avgt    6    0.916 ±   0.456  ms/op
JmhDemo.streamGenerate             10000000  avgt    6   26.783 ±   3.412  ms/op
JmhDemo.streamGenerate            100000000  avgt    6  191.738 ± 173.429  ms/op
JmhDemo.streamParallelGenerate       100000  avgt    6    0.104 ±   0.004  ms/op
JmhDemo.streamParallelGenerate      1000000  avgt    6    0.622 ±   0.022  ms/op
JmhDemo.streamParallelGenerate     10000000  avgt    6   23.461 ±  11.200  ms/op
JmhDemo.streamParallelGenerate    100000000  avgt    6   65.758 ±  44.910  ms/op

从结果看, for的消耗时间随着数据量增大而同比增大,成正比关系。而在千万数据上,流的性能突然下降,数据在亿级别,并行流性能更好

三、结果可视化

jmh 可通过将结果导出json数据

public static void main(String[] args) throws RunnerException {

    Options opt = new OptionsBuilder()
            .include(JmhDemo.class.getSimpleName())
            .result("E:\\list.json")
            .resultFormat(ResultFormatType.JSON)
            .build();

    new Runner(opt).run();
}

可以将Json数据上传两个网站,将结果可视化

四、IDEA插件

IDEA 提供插件 JMH Java Microbenchmark Harness,能够使用快捷键 Alt+Insert 或 MacOS Ctrl + N快速生成测试方法,还可以执行单个方法,类似 junit

在这里插入图片描述

参考

  1. java中的即时编译(JIT)简介
  2. jmh使用
  3. 基准测试神器JMH——详解36个官方例子
  4. JAVA拾遗 — JMH与8个代码陷阱
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

aabond

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值