JMH:Java微基准测试框架

Measure it , Don’t guess!

当我们写代码的时候总会遇到一些选择上的困惑,比如数据库连接池Druid和HikariCP到底哪一个效率更高,或者是LinkedList和ArrayList哪个在特定的场景下更快,再比如找到系统性能的瓶颈所在,但是具体是哪个方法哪条语句执行太慢而导致的。不要猜,请测试它

上面提到的场景都可以通过JMH(Java Microbenchmark Harness)来完成,JMH一个用于java或者其他JVM语言的,提供构建,运行和分析(按照多种基准:纳秒,微妙、毫秒)的工具。或者用比较晦涩解释是JMH是微基准测试框架。并且JMH将会作为JDK9的一部分。

这里特表标注下主要作者 Aleksey Shipilëv 的博客地址:https://shipilev.net/

下面介绍如何开始使用JMH。

如果不想讲JMH的依赖添加到我们的项目中,则可以创建一个新的项目专门用来JMH测试,将需要测试的代码引入即可。创建新的JMH项目命令如下:

mvn archetype:generate \
    -DinteractiveMode=false \
    -DarchetypeGroupId=org.openjdk.jmh \
    -DarchetypeArtifactId=jmh-java-benchmark-archetype \
    -DgroupId=io.four \
    -DartifactId=jmh-demo \
    -Dversion=1.0

当然如果不介意吧JHM引入项目,可以通过一下Maven依赖即可:

<properties>
    <jmh.version>1.17.4</jmh.version>
</properties>
<dependencies>
    <dependency>
        <groupId>org.openjdk.jmh</groupId>
        <artifactId>jmh-core</artifactId>
        <version>${jmh.version}</version>
    </dependency>
    <dependency>
        <groupId>org.openjdk.jmh</groupId>
        <artifactId>jmh-generator-annprocess</artifactId>
        <version>${jmh.version}</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

先看第一个例子,暂时不用管代码中的注解,为了能让下面代码启动,需要添加JMH的idea插件,右键即可启动。

@State(Scope.Thread)
public class MyBenchmark {

    @Benchmark
    @Warmup(iterations = 5)
    @Measurement(iterations = 5)
    @BenchmarkMode(Mode.Throughput)
    @OutputTimeUnit(TimeUnit.MILLISECONDS)
    @Fork(1)
    public long testMethod() {
        return System.currentTimeMillis();
    }
}
/* 结果:
# JMH 1.17.4 (released 783 days ago, please consider updating!)
# VM version: JDK 1.8.0_201, VM 25.201-b09
# VM invoker: /Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/bin/java
# VM options: -Dfile.encoding=UTF-8
# Warmup: 5 iterations, 1 s each
# Measurement: 5 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: io.four.MyBenchmark.testMethod

# Run progress: 0.00% complete, ETA 00:00:10
# Fork: 1 of 1
# Warmup Iteration   1: 32095.524 ops/ms
# Warmup Iteration   2: 32568.802 ops/ms
# Warmup Iteration   3: 32683.012 ops/ms
# Warmup Iteration   4: 32518.435 ops/ms
# Warmup Iteration   5: 32570.047 ops/ms
Iteration   1: 32530.326 ops/ms
Iteration   2: 32568.788 ops/ms
Iteration   3: 32764.806 ops/ms
Iteration   4: 32898.035 ops/ms
Iteration   5: 32784.544 ops/ms

Result "io.four.MyBenchmark.testMethod":
  32709.299 ±(99.9%) 596.970 ops/ms [Average]
  (min, avg, max) = (32530.326, 32709.299, 32898.035), stdev = 155.031
  CI (99.9%): [32112.330, 33306.269] (assumes normal distribution)

# Run complete. Total time: 00:00:10

Benchmark                Mode  Cnt      Score     Error   Units
MyBenchmark.testMethod  thrpt    5  32709.299 ± 596.970  ops/ms

结果中可以分为三块,分别为WarmUp/Iteration/Average,ops/ms表示每毫秒执行次数。

先来了解下JMH中的概念:
Fork
表示标记了BenchMark的方法测试几轮,每一轮都分为预热和正式测试部分。预热和正式测试包括多个Iteration。

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

Iteration
Iteration 是 JMH 进行测试的最小单位。在默认的情况下,一次 iteration 代表的是一秒,JMH 会在这一秒内不断调用需要 benchmark 的方法,然后根据模式对其采样,计算吞吐量,计算平均执行时间等。

Mode
Mode 表示 JMH 进行 Benchmark 时所使用的模式。通常是测量的维度不同,或是测量的方式不同。目前 JMH 共有四种模式:

  • Throughput: 整体吞吐量,例如“1秒内可以执行多少次调用”。
  • AverageTime: 调用的平均时间,例如“每次调用平均耗时xxx毫秒”。
  • SampleTime: 随机取样,最后输出取样结果的分布,例如“99%的调用在xxx毫秒以内,99.99%的调用在xxx毫秒以内”
  • SingleShotTime: 以上模式都是默认一次 iteration 是 1秒,唯有 SingleShotTime 是只运行一次。往往同时把 warmup 次数设为0,用于测试冷启动时的性能。

下面解释下上面代码中用到的注解:
@State
State标注在某一个类上,用于声明该类型对象的范围,我们测试标注为benchmark的方法,其实就是创建该类的实例对象过,然后通过多线程调用此方法。
如果标注为@State(Scope.Thread),则为每一个线程创建一个对象,如果标注为@State(Scope.Benchmark),则创建一个对象,所有线程间共享。Scope.Group:每个线程组共享一个实例。

@Benchmark
@Benchmark标签是用来标记测试方法的,只有被这个注解标记的话,该方法才会参与基准测试,但是有一个基本的原则就是被@Benchmark标记的方法必须是public的。安装了JMH idea插件才可以直接执行,否则需要通过main方法,后面介绍。

@Warmup
@Warmup用来配置预热的内容,可用于类或者方法上,越靠近执行方法的地方越准确。一般配置warmup的参数有这些:

  • iterations:预热的次数
  • time:每次预热的时间,默认是1。
  • timeUnit:时间单位,默认是s,默认是sec。
  • batchSize:批处理大小,每次操作调用几次方法。

@Measurement
用来控制实际执行的内容,配置的选项本warmup一样。

@BenchmarkMode
表示 JMH 进行 Benchmark 时所使用的模式。

@OutputTimeUnit
@OutputTimeUnit代表测量的单位,比如秒级别,毫秒级别,微妙级别等等。一般都使用微妙和毫秒级别的稍微多一点。该注解可以用在方法级别和类级别,当用在类级别的时候会被更加精确的方法级别的注解覆盖,原则就是离目标更近的注解更容易生效。

@Thread
使用线程数,一般设置为cpu的核心数。

@Setup
@Setup 会在执行 benchmark 之前被执行,正如其名,主要用于初始化。

@TearDown
@TearDown 和 @Setup 相对的,会在所有 benchmark 执行结束以后执行,主要用于资源的回收等。

@Group
方法注解,可以把多个 benchmark 定义为同一个 group,则它们会被同时执行,譬如用来模拟生产者-消费者读写速度不一致情况下的表现。使用@GroupThreads(threadsNumber)注解标记每个测试,指定运行给定方法的线程数量。默认是1。

@Param
成员注解,可以用来指定某项参数的多种情况。特别适合用来测试一个函数在不同的参数输入的情况下的性能。@Param注解接收一个String数组,在@setup方法执行前转化为为对应的数据类型。多个@Param注解的成员之间是乘积关系,譬如有两个用@Param注解的字段,第一个有5个值,第二个字段有2个值,那么每个测试方法会跑5*2=10次。

通过插件启动每次只能测试一个标注为benchmark的方法,假如需要有多个方法则只能通过main方法启动了:

@State(Scope.Thread)
public class BenchMarkDEMO {

    @Benchmark
    public long test1() {
        return System.currentTimeMillis();
    }

    @Benchmark
    public long test2() {
        return Calendar.getInstance().getTimeInMillis();
    }

    public static void main(String[] args) throws RunnerException {
        Options ops = new OptionsBuilder()
                // include @benchmark 所在的类的名字
                .include(BenchMarkDEMO.class.getName())
                .forks(1)
                // 预热的次数
                .warmupIterations(5)
                // 实际测量的迭代次数。
                .measurementIterations(5)
                .threads(2)
                .build();
        new Runner(ops).run();
    }
}

前面这些是JMH的基本用法,下面讲述关于一些容易才踩的坑

1.消除无用代码

@Benchmark
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public void test() {
}

@Benchmark
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public void test1() {
     int a = 1;
     int b = 2;
     int sum = a + b;
}

@Benchmark
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public int test2() {
    int a = 1;
    int b = 2;
    int sum = a + b;
    return sum;
}
/*结果为:
Benchmark             Mode  Cnt     Score     Error   Units
BenchMarkDEMO.test   thrpt    5  6745.091 ± 568.358  ops/us
BenchMarkDEMO.test1  thrpt    5  6934.955 ± 374.061  ops/us
BenchMarkDEMO.test2  thrpt    5   977.161 ±   5.758  ops/us
*/

可以看出test和test1方法几乎一样,test1方法虽然有逻辑,但是在JVM看来是无用代码,并且将其消除。但是这和我们测试代码的初衷有出入。
test2方法和我们预期差不多。返回结果值是一种解决无用代码消除的方式,还有就是利用JMH提供的Blackhole,代码如下:

@Benchmark
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public void test1(Blackhole blackhole) {
     int a = 1;
     int b = 2;
     int sum = a + b;
     blackhole.consume(sum);
}

2.方法内联
方法内联属于JIT优化,可以在JHM测试中禁用或者启用它

CompilerControl.Mode.DONT_INLINE:强制限制不能使用内联
CompilerControl.Mode.INLINE:强制使用内联

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值