Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JMH基准测试和JMH-Visual-chart可视化 #68

Open
Sayi opened this issue Jan 10, 2019 · 0 comments
Open

JMH基准测试和JMH-Visual-chart可视化 #68

Sayi opened this issue Jan 10, 2019 · 0 comments

Comments

@Sayi
Copy link
Owner

Sayi commented Jan 10, 2019

如何度量一段代码的性能,换种实现方式会有更佳的性能表现吗?你或许想知道fastjson是否正如它自己所说的那样至今性能未遇对手?Fork/Join框架真的有提高性能吗?

一句话:Measure, Don’t Guess!

JMH(Java Microbenchmark Harness)是由OpenJDK Developer提供的基准测试工具(基准可以理解为比较的基础,我们将这一次性能测试结果作为基准结果,下一次的测试结果将与基准数据进行比较),它是一种常用的性能测试工具,解决了基准测试中常见的一些问题,本文将针对这些问题介绍如何正确的使用JMH,以及可视化测试结果。

可视化JMH Visual chart GitHub地址:https://github.com/Sayi/jmh-visual-chart

字符串拼接性能比较

我们通过基准测试来比较使用"+"号和使用Stringbuilder进行字符串拼接的性能。

1. 创建基准测试项目

我们可以在一个已有项目中运行基准测试,但是为了获得更加准确的度量结果,官方推荐使用Maven archetype来创建独立的JMH项目:

mvn archetype:generate \
          -DinteractiveMode=false \
          -DarchetypeGroupId=org.openjdk.jmh \
          -DarchetypeArtifactId=jmh-java-benchmark-archetype \
          -DgroupId=com.deepoove \
          -DartifactId=hello-mh \
          -Dversion=1.0.0-SNAPSHOT

这样就创建了一个hello-mh的Maven JMH项目。

2. 编写基准测试代码

package com.deepoove;
import org.openjdk.jmh.annotations.Benchmark;

@BenchmarkMode(Mode.Throughput)
@Measurement(iterations = 2, time = 6, timeUnit = TimeUnit.SECONDS)
@Threads(4)
@Fork(2)
@Warmup(iterations = 1)
@State(value = Scope.Benchmark)
public class MyBenchmark {

  @Param(value = { "10", "50", "100" })
  private int length;

  @Benchmark
  public void testStringAdd(Blackhole blackhole) {
    String a = "";
    for (int i = 0; i < length; i++) {
      a += i;
    }
    blackhole.consume(a);
  }

  @Benchmark
  public void testStringBuilderAdd(Blackhole blackhole) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < length; i++) {
      sb.append(i);
    }
    blackhole.consume(sb.toString());
  }
}

这段用到了很多注解,我们姑且不去理会,把重点放在方法级别的注解@Benchmark,JMH会找到@Benchmark注解的方法进行基准测试,方法可以有多个,JMH会依次测试这些方法。

3. 编译和执行基准测试

我们可以通过JMH的API来启动基准测试,在MyBenchmark类中增加main方法:

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

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

如果在运行时报错Exception in thread "main" No benchmarks to run;,需要执行Maven命令进行编译:

mvn clean compile

基准测试的结果会在控制台打印出来,一开始就读懂这份结果并不简单,我们先来熟悉下JMH提供的注解和用法。

JMH基准测试

度量模式:@BenchmarkMode

一个最典型最原始的性能度量方式是比较时间差,如下面这段代码所示:

long start = System.currentTimeMillis();
doSomethings(); //执行你要度量的代码
long end = System.currentTimeMillis();
System.out.println("time: " + (end - start) + " milliseconds.");

但是它有一定的问题,System.currentTimeMillis()并不精准,根据不同系统环境会有一定幅度的误差,System.nanoTime()可以提供相对精确的计时,但是也有一定的偏移量,而且只用单次测量的结果作为标准也是不可信的。

JMH提供了注解@BenchmarkMode,可以基于多次度量生成结果:

  • @BenchmarkMode(Mode.Throughput)
    吞吐量,单位时间内执行操作的次数,结果的单位是ops/time。
  • @BenchmarkMode(Mode.AverageTime)
    平均时间,平均每次操作的耗时,结果的单位是time/ops。

还有更多的模式(Mode.SampleTimeMode.SingleShotTimeMode.All)可以设置,详情参阅Javadoc。

预热:@Warmup

预热是指让你的测试代码在正式收集数据前先跑一定次数,因为第一次运行包含了类加载和初始化等影响测试结果的过程,所以永远需要预热你的代码,JMH提供注解@Warmup来设置预热参数。

@Warmup(iterations = 5)

这行代码表示预热次数为5。

测量方式:@Measurement

JMH是基于多次测量的结果,可以通过注解@Measurement设定多次测量的方式。

@Measurement(iterations = 5, time = 10, timeUnit = TimeUnit.SECONDS)

这行代码表示测量5次,每次测量时间为10秒。

循环执行:@Fork

有时候想结合多轮Benchmark的测试结果进行分析,这样就可以用到@Fork注解。

@Fork(2)

这行代码表示Benchmark的测试会运行两轮。

参数组合:@Param@State

我们可能想度量不同参数组合下某个方法的性能表现,这时候就可以使用@Param来列举这些参数值。

@Param(value = { "10", "50", "100" })
private int length;

这行代码设置就会依次执行lenght=10,50,100时候的基准测试方法。

如果只是用@Param在编译时会报错,它必须配合@State注解使用,@State指定了对象共享范围。

  • @State(value = Scope.Benchmark):基准测试内共享对象
  • @State(value = Scope.Group):同一个线程组内共享
  • @State(value = Scope.Thread):同一个线程内共享

初始化和销毁:@Setup & @TearDown

假如初始化和销毁代码并不是基准测试的一部分,为了减少测试噪音,所以不应该放到@Benchmark修饰的方法内部,JMH提供了@Setup@TearDown实现这样的功能。

避免死代码消除DCE:Dead Code Elimination

有时候一段代码最终执行的时候并不是我们看到的那个样子,对于死代码编译器会进行优化。如果我们把字符串拼接的示例代码改成这样:

@Benchmark
public void testStringAdd() {
    String a = "";
    for (int i = 0; i < length; i++) {
        a += i;
    }
}

JVM可能会认为变量a从来没有使用过,从而进行优化把整个方法内部代码移除掉,显然,这影响了测试结果。

JMH提供了两种方式避免这种问题,一种是将这个变量作为方法返回值return a,一种是通过Blackhole类来消费这个变量:

blackhole.consume(a);

避免常量折叠:Constant Folding

当基于常量的操作结果是一定的,JVM也会进行优化,我们看下面的一个例子:

private double x = Math.PI;

private final double wrongX = Math.PI;

@Benchmark
public double baseline() {
    return Math.PI;
}

@Benchmark
public double measureWrong_1() {
    // 这里会引起常量折叠优化,Math.log(Math.PI)的结果是可预测的
    return Math.log(Math.PI);
}

@Benchmark
public double measureWrong_2() {
    // 这里会引起常量折叠优化,Math.log(wrongX)的结果是可预测的
    return Math.log(wrongX);
}

@Benchmark
public double measureRight() {
    // 这是正确的,基于变量x的代码是不可预测的
    return Math.log(x);
}

不建议直接引用常量,我们可以通过@State注解类中的变量去引用,就像下面这段代码:

@State(Scope.Thread)
public static class MyState {
    public int a = Math.PI;
}

@Benchmark 
public int testMethod(MyState state) {
    int sum = state.a + 10;
    return sum;
}

JMH Visual chart基准测试可视化

解析基准测试结果

我们再次回看字符串拼接基础测试性能结果,可以比较清晰的看到整个分析的过程:

// 方法testStringAdd 参数length=10的基准测试
# JMH version: 1.21
# VM version: JDK 1.8.0_131, Java HotSpot(TM) 64-Bit Server VM, 25.131-b11
# VM invoker: /Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/bin/java
# VM options: -Dfile.encoding=UTF-8
# Warmup: 1 iterations, 10 s each // 预热
# Measurement: 2 iterations, 6 s each // 测量
# Timeout: 10 min per iteration 
# Threads: 4 threads, will synchronize iterations // 线程数
# Benchmark mode: Throughput, ops/time // 度量方式
# Benchmark: com.deepoove.MyBenchmark.testStringAdd // 执行的方法
# Parameters: (length = 10) // 参数组合

// fork1 的预热和基准测试
# Run progress: 0.00% complete, ETA 00:04:24
# Fork: 1 of 2
# Warmup Iteration   1: 7908426.420 ops/s
Iteration   1: 7257469.806 ops/s
Iteration   2: 8570196.109 ops/s

// fork2 的预热和基准测试
# Run progress: 8.33% complete, ETA 00:05:09
# Fork: 2 of 2
# Warmup Iteration   1: 7655259.376 ops/s
Iteration   1: 6372627.794 ops/s
Iteration   2: 4954086.450 ops/s

// 结果
Result "com.deepoove.MyBenchmark.testStringAdd":
  6788595.040 ±(99.9%) 9823071.462 ops/s [Average]
  (min, avg, max) = (4954086.450, 6788595.040, 8570196.109), stdev = 1520131.182
  CI (99.9%): [≈ 0, 16611666.501] (assumes normal distribution)

// ...略

// 最终结果的比较
# Run complete. Total time: 00:05:32

REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.

Benchmark                         (length)   Mode  Cnt         Score         Error  Units
MyBenchmark.testStringAdd               10  thrpt    4   6788595.040 ± 9823071.462  ops/s
MyBenchmark.testStringAdd               50  thrpt    4   1261762.676 ±  542791.113  ops/s
MyBenchmark.testStringAdd              100  thrpt    4    379271.146 ±   25933.030  ops/s
MyBenchmark.testStringBuilderAdd        10  thrpt    4  18271291.690 ± 7799119.896  ops/s
MyBenchmark.testStringBuilderAdd        50  thrpt    4   2958957.096 ± 1216254.086  ops/s
MyBenchmark.testStringBuilderAdd       100  thrpt    4   1461698.122 ±  499953.566  ops/s

最后六行表明:执行10、50、100次字符串拼接,testStringBuilderAdd在单位时间执行次数都优于testStringAdd。

jmh-visual-chart

jmh-visual-chart支持上传JMH的JSON结果文件然后解析成图表,实现原理很简单,将基准测试的JSON数据转化成图表需要的数据即可。

我们将字符串拼接基准测试代码的main方法改造下,支持JSON文件的输出:

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

    new Runner(opt).run();
}

将结果文件result.json上传至jmh-visual-chart生成图表:

image

总结

JMH是个人人需要掌握的基准测试工具,JMH visual chart这个项目目前处在实验状态,并没有对所有可能的基准测试结果进行验证,目前它能够比较不同参数下不同方法的性能,未来可以无限的扩展JSON to Chart的转化方法从而支持更多的图表。

最后推荐下JMH Visualizer,它是一个功能齐全的可视化项目,只是少了我想要的图表罢了。

参考资料

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant