JMH探索

JMH探索

  • 一、JMH基本介绍

    1.1 什么是JMH

    1.2 JMH入门
  • 二、JMH的基本概念和配置

    2.1 例

    2.2 基本标签介绍

    2.3 控制台输出

    2.4 常用模式(Mode)

    2.5 迭代(Iteration)

    2.6 预热(Warmup)

    2.7 配置类(Options)

    2.8 状态(State)
  • 三、JMH注意事项

    3.1 Dead-Code代码

    3.2 黑洞

    3.3 常量折叠

    3.4 避免循环

    3.5 分叉
  • 四、可视化

JMH基本介绍

什么是JMH

    
JMH,即Java Microbenchmark Harness,是专门用于Java代码微基准测试的工具套件。由OpenJDK开发的,主要是基于方法层面的基准测试,精度可以达到纳秒级。当定位到热点方法,希望进一步优化方法性能的时候,就可以使用JMH对优化的结果进行量化分析。

JMH入门

    
使用maven项目演示,这里使用最新版本1.28

<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-core</artifactId>
    <version>1.28</version>
</dependency>
<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-generator-annprocess</artifactId>
    <version>1.28</version>
    <scope>provided</scope>
</dependency>
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public class JMHSample_01_HelloWorld {
    @Benchmark
    public void wellHelloThere() {
        // this method was intentionally left blank.
    }

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(JMHSample_01_HelloWorld.class.getSimpleName())
                .build();

        new Runner(opt).run();
    }
}

    
以上便是官方最简单的测试,其中度量的方法为wellHelloThere,执行后可判断出该方法的测试基础信息(个人不建议执行,默认的参数执行的时间太长)。

JMH的基本概念和配置

    
我们再看一个稍微复杂的例子,比对int和Integer的性能:

@BenchmarkMode(Mode.Throughput)
@Warmup(iterations = 1, time = 1)
@Measurement(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)
@Threads(1)
@Fork(1)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class MMBenchmarkInt {

    @Benchmark
    public int mmTestInt() {
        int result = 0;
        for (int i = 0; i < 100; i++) {
            result += i;
        }
        return result;
    }

    @Benchmark
    public Integer mmTestInteger() {
        Integer result = 0;
        for (int i = 0; i < 100; i++) {
            result += i;
        }
        return result;
    }

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(MMBenchmarkInt.class.getSimpleName())
                .build();

        new Runner(opt).run();
    }
}

基本标签介绍

@BenchmarkMode为使用模式,参数Mode表示JMH的测量方式和角度,有以下4种:

  • Throughput:吞吐量,单位时间内可以执行多少次。
  • AverageTime:平均时间。
  • SampleTime:随机取样,最后输出取样结果的分布,如99%的调用在xx毫秒内,99.99的调用在xx毫秒内。
  • SingleShotTime:只运行一次,一般用于测试冷启动的消耗时间。
  • All:统计前面所有的指标。

@Warmup为配置预热次数,本例是每次执行1秒,执行1次。


@Measurement为配置执行次数,本例为每次执行1秒,执行3次。在性能对比时,采用默认1秒即可,如果用jvisualvm来做性能监控,则可指定一个较长的运行时间。


@Threads为配置多少个线程同时执行,


@Fork为启动单独的JVM进程分别测试每个方法,这里指定为每个方法启动1个进程。


@OutputTimeUnit为统计结果的时间单元,本例是TImeUnit.NANOSECONDS,运行后会看到输出结果是统计的每纳秒的吞吐量。


控制台输出

    
开始会输出本次测试的参数:预测次数、执行次数、测量模式、测试的方法等等。

# Warmup: 1 iterations, 1 s each
# Measurement: 3 iterations, 1 ns each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: com.mm.mmspringboot.test.MMBenchmarkString.mmTest1

    
接着,这里的Fork表示子进程,我们只配置了一个,因此只有一个进程的执行结果;然后下面包含了Warmup(预热1次,每次1秒);再下面为Measurement(执行3次,每次1秒)。

# Run progress: 0.00% complete, ETA 00:00:08
# Fork: 1 of 1
# Warmup Iteration   1: 0.462 ops/ns
Iteration   1: 0.455 ops/ns
Iteration   2: 0.595 ops/ns
Iteration   3: 0.592 ops/ns

    
执行完每个测试方法,都会打印一个每个方法的汇总信息。

    
汇总信息包含了多次测试后的最小值、最大值、均值、标准差(stdev)和置信区间(CI,Confidence interval)。

Result "com.mm.mmspringboot.test.MMBenchmarkInt.mmTestInt":
  0.547 ±(99.9%) 1.459 ops/ns [Average]
  (min, avg, max) = (0.455, 0.547, 0.595), stdev = 0.080
  CI (99.9%): [≈ 0, 2.007] (assumes normal distribution)

    
最后,JMH会为我们打印出多个方法的测试对比结果,可看出int比Integer的吞吐量高许多:

Benchmark                      Mode  Cnt  Score   Error   Units
MMBenchmarkInt.mmTestInt      thrpt    3  0.547 ± 1.459  ops/ns
MMBenchmarkInt.mmTestInteger  thrpt    3  0.004 ± 0.002  ops/ns

    
该例中测量模式为吞吐量

常用模式(Mode)

    
上例中,我们使用的测试模式为Mode.Throughput(吞吐量),单位为ops(Operation Per Second),结合输出单位,就是每纳秒的吞吐量。

    
下面我们来看看其他测量模式的比对结果,Mode.AverageTime(平局时间),单位为ns/op,即每次吞吐的纳秒,结果如下:

Benchmark                     Mode  Cnt    Score     Error  Units
MMBenchmarkInt.mmTestInt      avgt    3    1.842 ±   4.844  ns/op
MMBenchmarkInt.mmTestInteger  avgt    3  261.227 ± 380.288  ns/op

    
Mode.SampleTime(随机采样),可看到分布图(如p0.99的Score为100,表名了99%的每次吞吐的纳秒在100以内),分布结果如下:

JMH探索_第1张图片

迭代(Iteration)

    
可看到在Warmup(预热)和Measurement(执行)中都用到了iterations,它是JMH的测量单位,每次迭代都会统计出相关的吞吐量、平均时间等。

预热(Warmup)

    
由于Java虚拟机的JIT的存在,同一个方法在JIT编译前后的时间将会不同。通常只考虑方法在JIT编译后的性能,所以需要预热。

配置类(Options)

    
配置类用于指定一些参数,如指定测试类(include)、使用的进程个数(fork)、执行迭代次数(measurementIterations)等等。

    
如上例中的注解属性:

@BenchmarkMode(Mode.Throughput)
@Warmup(iterations = 1, time = 1)
@Measurement(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)
@Threads(1)
@Fork(1)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class MMBenchmarkProperties {

    
完全可在配置类里声明:

public static void main(String[] args) throws RunnerException {
    Options opt = new OptionsBuilder()
            .include(MMBenchmarkProperties.class.getSimpleName())
            .mode(Mode.Throughput)
            .warmupIterations(1).warmupTime(new TimeValue(1, TimeUnit.SECONDS))
            .measurementIterations(3).measurementTime(new TimeValue(1, TimeUnit.SECONDS))
            .threads(1)
            .forks(1)
            .timeUnit(TimeUnit.NANOSECONDS)
            .build();

    new Runner(opt).run();
}

状态(State)

通过State指定一个对象的作用范围(Scope),取值如下:

  • Scope.Benchmark:基准测试范围,多个线程共享一个实例,可用于测试多线程共享下的性能。
  • Scope.Thread:默认的 State,每个测试线程分配一个实例。
  • Scope.Group:同一个线程在同一个 group 里共享实例。

    
使用官方的例子改改,测试如下:

@BenchmarkMode(Mode.Throughput)
@Warmup(iterations = 1, time = 1)
@Measurement(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)
@Threads(5)
@Fork(1)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class JMHSample_03_States {
    @State(Scope.Benchmark)
    public static class BenchmarkState {
        volatile double x = Math.PI;
    }

    @State(Scope.Thread)
    public static class ThreadState {
        volatile double x = Math.PI;
    }

    @Benchmark
    public void ThreadScope(BenchmarkState state) {
        state.x++;
    }

    @Benchmark
    public void BenchmarkScope(ThreadState state) {
        state.x++;
    }

    @State(Scope.Group)
    public static class GroupState {
        volatile double x = Math.PI;
    }

    @Benchmark
    @Group("mmGroup")
    @GroupThreads(4)
    public double GroupScopeRead(GroupState state) {
        return state.x;
    }

    @Benchmark
    @Group("mmGroup")
    @GroupThreads(1)
    public void GroupScopeWrite(GroupState state) {
        state.x++;
    }

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(JMHSample_03_States.class.getSimpleName())
                .build();

        new Runner(opt).run();
    }
}
Benchmark                Mode  Cnt  Score   Error   Units
BenchmarkScope           thrpt    3  0.871 ± 0.054  ops/ns
ThreadScope              thrpt    3  0.042 ± 0.002  ops/ns
mmGroup                  thrpt    3  0.333 ± 0.033  ops/ns
mmGroup:GroupScopeRead   thrpt    3  0.294 ± 0.038  ops/ns
mmGroup:GroupScopeWrite  thrpt    3  0.039 ± 0.007  ops/ns

    
控制台输出中,BenchmarkScope和ThreadScope代表着多线程和单线程下的比对结果。mmGroup代表着是一个组,这里为了测试比对数据,通过@GroupThreads分配了读4个线程,写1个线程。

JMH中的一些陷阱

Dead-Code代码

    
编写JMH用例时需要考虑虚拟机的优化,避免性能测试结果不准。下例由于measureWrong并没有返回值,所以虚拟机会优化掉这个方法。

...省略头信息
public class JMHSample_08_DeadCode {
    private double x = Math.PI;

    @Benchmark
    public void baseline() {
        //基准线,什么都不做
    }

    @Benchmark
    public void measureWrong() {
        //错误:结果未被使用,整个方法被优化掉了
        Math.log(x);
    }

    @Benchmark
    public double measureRight() {
        //正确:结果被使用
        return Math.log(x);
    }
    ...省略运行信息
}
Benchmark                            Mode  Cnt  Score   Error   Units
JMHSample_08_DeadCode.baseline      thrpt    3  4.188 ± 0.840  ops/ns
JMHSample_08_DeadCode.measureRight  thrpt    3  0.060 ± 0.012  ops/ns
JMHSample_08_DeadCode.measureWrong  thrpt    3  3.405 ± 1.065  ops/ns

黑洞

    
使用Blackholes来避免JIT忽略未被使用的信息。

...省略头信息
public class JMHSample_09_Blackholes {
    double x1 = Math.PI;
    double x2 = Math.PI * 2;

    /*
     * 基准
     */
    @Benchmark
    public double baseline() {
        return Math.log(x1);
    }

    /*
     * Math.log(x2)正常,Math.log(x1)被优化掉
     */

    @Benchmark
    public double measureWrong() {
        Math.log(x1);
        return Math.log(x2);
    }

    @Benchmark
    public double measureRight_1() {
        return Math.log(x1) + Math.log(x2);
    }

    /*
     * 使用Blackhole对象将值存储这里,避免被优化掉
     */
    @Benchmark
    public void measureRight_2(Blackhole bh) {
        bh.consume(Math.log(x1));
        bh.consume(Math.log(x2));
    }
    ...省略运行信息
}
Benchmark                                Mode  Cnt  Score   Error   Units
JMHSample_09_Blackholes.baseline        thrpt    3  0.058 ± 0.026  ops/ns
JMHSample_09_Blackholes.measureRight_1  thrpt    3  0.028 ± 0.050  ops/ns
JMHSample_09_Blackholes.measureRight_2  thrpt    3  0.026 ± 0.018  ops/ns
JMHSample_09_Blackholes.measureWrong    thrpt    3  0.055 ± 0.024  ops/ns

常量折叠

    
JIT认为被测试方法总是返回常量,从而在优化时直接返回常量给调用者而不再调用方法。

...省略头信息
public class JMHSample_10_ConstantFold {
    private double x = Math.PI;

    private final double wrongX = Math.PI;

    @Benchmark
    public double baseline() {
        // 基准线
        return Math.PI;
    }

    @Benchmark
    public double measureWrong_1() {
        //错误:源是可预见的,计算是可折叠的
        return Math.log(Math.PI);
    }

    @Benchmark
    public double measureWrong_2() {
        //错误:源是可预见的,计算是可折叠的
        return Math.log(wrongX);
    }

    @Benchmark
    public double measureRight() {
        //正确,源是不可预见的
        return Math.log(x);
    }
    ...省略运行信息
}
Benchmark                                  Mode  Cnt  Score   Error   Units
JMHSample_10_ConstantFold.baseline        thrpt    3  0.537 ± 1.565  ops/ns
JMHSample_10_ConstantFold.measureRight    thrpt    3  0.060 ± 0.019  ops/ns
JMHSample_10_ConstantFold.measureWrong_1  thrpt    3  0.422 ± 1.372  ops/ns
JMHSample_10_ConstantFold.measureWrong_2  thrpt    3  0.406 ± 1.132  ops/ns

避免循环

    
JMH不建议使用循环,因为JIT会种循环做优化,以消除循环调用成本。

...省略头信息
public class JMHSample_11_Loops {
    int x = 1;
    int y = 2;

    @Benchmark
    public int measureRight() {
        return (x + y);
    }

    private int reps(int reps) {
        int s = 0;
        for (int i = 0; i < reps; i++) {
            s += (x + y);
        }
        return s;
    }

    @Benchmark
    @OperationsPerInvocation(1)
    public int measureWrong_1() {
        return reps(1);
    }

    @Benchmark
    @OperationsPerInvocation(10)
    public int measureWrong_10() {
        return reps(10);
    }

    @Benchmark
    @OperationsPerInvocation(100)
    public int measureWrong_100() {
        return reps(100);
    }

    @Benchmark
    @OperationsPerInvocation(1_000)
    public int measureWrong_1000() {
        return reps(1_000);
    }

    @Benchmark
    @OperationsPerInvocation(10_000)
    public int measureWrong_10000() {
        return reps(10_000);
    }

    @Benchmark
    @OperationsPerInvocation(100_000)
    public int measureWrong_100000() {
        return reps(100_000);
    }
    ...省略运行信息
}

    
上例中,@OperationsPerInvocation为每次Benchmark调用的操作数,其告诉JMH统计性能的时候需要做修正,比如@OperationsPerInvocation(10)调用10次。

Benchmark                                Mode  Cnt   Score     Error   Units
JMHSample_11_Loops.measureRight         thrpt    3   0.439 ±   0.447  ops/ns
JMHSample_11_Loops.measureWrong_1       thrpt    3   0.392 ±   0.757  ops/ns
JMHSample_11_Loops.measureWrong_10      thrpt    3   3.755 ±   3.118  ops/ns
JMHSample_11_Loops.measureWrong_100     thrpt    3  30.362 ±  63.189  ops/ns
JMHSample_11_Loops.measureWrong_1000    thrpt    3  34.975 ± 161.861  ops/ns
JMHSample_11_Loops.measureWrong_10000   thrpt    3  51.644 ±  18.460  ops/ns
JMHSample_11_Loops.measureWrong_100000  thrpt    3  56.236 ±  13.568  ops/ns

分叉

    
默认情况下,JMH是分叉测试的。

...省略头信息
public class JMHSample_12_Forking {
    public interface Counter {
        int inc();
    }

    public static class Counter1 implements Counter {
        private int x;

        @Override
        public int inc() {
            return x++;
        }
    }

    public static class Counter2 implements Counter {
        private int x;

        @Override
        public int inc() {
            return x++;
        }
    }

    /**
     * And this is how we measure it.
     * Note this is susceptible for same issue with loops we mention in previous examples.
     */

    public int measure(Counter c) {
        int s = 0;
        for (int i = 0; i < 10; i++) {
            s += c.inc();
        }
        return s;
    }

    Counter c1 = new Counter1();
    Counter c2 = new Counter2();

    //首先单独测量Counter1
    //Fork(0)帮助在同一JVM中运行
    @Benchmark
    @Fork(0)
    public int measure_1_c1() {
        return measure(c1);
    }

    //单独Counter2
    @Benchmark
    @Fork(0)
    public int measure_2_c2() {
        return measure(c2);
    }

    //单独Counter1
    @Benchmark
    @Fork(0)
    public int measure_3_c1_again() {
        return measure(c1);
    }

    /*
     * 下面两个是带有@Fork注解。
     * JMH将此注解作为在forked JVM中运行测试的请求,通过命令选项“-f”强制对所有测试执行此行为更简单。
     * forking是默认的,但我们仍使用注释来保持一致性。
     */
    @Benchmark
    @Fork(1)
    public int measure_4_forked_c1() {
        return measure(c1);
    }

    @Benchmark
    @Fork(1)
    public int measure_5_forked_c2() {
        return measure(c2);
    }

    /*
     * 结果可看到fork(0)的方法会越来越慢,因为它们是在同一JVM中运行,相互影响。
     * c1,c2,c1_again 的实现相同,跑分却不同,因为运行在同一个 JVM 中;而 forked_c1 和 forked_c2 则表现出了一致的性能。
     */
    ...省略运行信息
}
Benchmark                                  Mode  Cnt  Score   Error   Units
JMHSample_12_Forking.measure_1_c1         thrpt    3  0.508 ± 0.298  ops/ns
JMHSample_12_Forking.measure_2_c2         thrpt    3  0.052 ± 0.199  ops/ns
JMHSample_12_Forking.measure_3_c1_again   thrpt    3  0.075 ± 0.072  ops/ns
JMHSample_12_Forking.measure_4_forked_c1  thrpt    3  0.350 ± 0.521  ops/ns
JMHSample_12_Forking.measure_5_forked_c2  thrpt    3  0.346 ± 0.502  ops/ns

    
JMH基本介绍到这里就结束了。如果大家需要更多的入门实例,可参照官方地址:

http://hg.openjdk.java.net/code-tools/jmh/file/
https://github.com/openjdk/jmh/tree/master/jmh-samples

可视化

    
JMH的可视化非常简单,只需要在运行时导出为json格式的报告,然后再利用可视化工具分析即可,具体如下:

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

    
运行后,项目下会生成一个result.json的结果报告。
目前JMH的在线可视化主要有如下两款工具,将json结果报告导入即可:

https://jmh.morethan.io/
http://deepoove.com/jmh-visual-chart/

你可能感兴趣的:(JMH,java)