• 1、概览
  • 2、jmh 简介
  • 3、jmh 使用demo
  • 4、jmh 常用设置介绍
  • 5、注意事项
你的努力,终将成就无可替代的自己
将来的你一定会感谢现在拼命的自己

1、概览

在日常开发中,我们往往需要优化我们自己写的代码。优化后的代码,执行效率是否比之前的还高?具体高多少?这些都是需要去测量。
目前比较主流的做法是使用 jmh 进行微基准测试。

2、jmh 简介

jmhjava 用于微基准测试工具套件。主要是基于方法层面的基准测试,精度可达纳秒级。由 oracle 实现 JIT 大牛编写而成。

在使用 jmh 之前,我们往往会先通过各种工具(jvisualvm)找到热点代码, 然后再对热点代码使用 jmh 进行量化分析。

3、jmh 使用demo

下面使用字符串拼接作为案例介绍

第一步:加入依赖

maven 中引入 jmh jar

<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-core</artifactId>
    <version>1.0</version>
</dependency>

<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-generator-annprocess</artifactId>
    <version>1.0</version>
    <scope>provided</scope>
</dependency>
第二步:编写基准测试

接下来,创建测试类,来判断 + 还是 StringBuilder.append() 吞吐量更高

@BenchmarkMode(Mode.Throughput)
@Warmup(iterations = 3)
@Measurement(iterations = 3, time = 5, timeUnit = TimeUnit.SECONDS)
@Threads(1)
@Fork(1)
@State(Scope.Benchmark)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class StringBenchmark {

    String a = "1";
    String b = "2";
    String c = "3";

    @Benchmark
    public String builderBenchmark() {
        return new StringBuilder().append(a).append(b).append(c).toString();
    }

    @Benchmark
    public String connectionBenchmark() {
        return a + b + c;
    }

    public static void main(String[] args) throws RunnerException {
        Options options = new OptionsBuilder()
                .include(StringBenchmark.class.getSimpleName())
                .build();
        new Runner(options).run();
    }
}
第三步:查看执行结果
# Warmup: 3 iterations, 1 s each
# Measurement: 3 iterations, 5 s each
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: com.csp.boot.jmh.StringBenchmark.builderBenchmark

以上输出来自于我们的配置。
第一行表示预热 3 次,每次 1 秒。
第二行表示运行 3 次,每次运行 5 秒。
第三行表示 1 个线程运行
第四行表示统计的数据纬度为吞吐量


# Run progress: 0.00% complete, ETA 00:00:36
# Fork: 1 of 1
# Warmup Iteration   1: 27694.373 ops/ms
# Warmup Iteration   2: 47351.819 ops/ms
# Warmup Iteration   3: 60008.968 ops/ms
Iteration   1: 65411.091 ops/ms
Iteration   2: 64443.826 ops/ms
Iteration   3: 65067.621 ops/ms

# Fork 表示子进程。因为只配置了 1 个,所以只有一个进程执行结果。
# Warmup Iteration 为预热的数据,不会被计入统计,我们配置了 3 次预热,所以有 3 个结果。
Iteration 方法执行的结果


Result: 64974.180 ±(99.9%) 8945.921 ops/ms [Average]
  Statistics: (min, avg, max) = (64443.826, 64974.180, 65411.091), stdev = 490.356
  Confidence interval (99.9%): [56028.259, 73920.100]

统计结果给出了多次测量后的最小值、均值,最大值,以及标准差(stdev),置信区间。


Benchmark             Mode   Samples  Score      Score error   Units
builderBenchmark      thrpt    3     64974.180     8945.921    ops/ms
connectionBenchmark   thrpt    3     63524.697    69103.252    ops/ms

在最后,会给出 2 个基准测试的性能对比。
从上面结果来看,使用 +StringBuilder.append() 吞吐量差不多,原因在于,+ 在编译时,会使用 StringBuilder.append() 追加字符。

4、jmh 常用设置介绍

@BenchmarkMode
BenchmarkMode 为使用模式,可选值如下:
Mode.Throughput:吞吐量模式
AverageTime: 表示每次执行时间
SampleTime: 表示采样时间
SingleShotTime: 表示只运行一次,用于测试冷启动消费时间
All: 表示统计前面所有指标
@Warmup
配置预热次数,本例是 3
@Measurement
配置执行次数,本例是运行 5 秒,总共执行 3 次。如果是做性能测试,默认使用 1 秒即可
@Threads
配置同时执行多少个线程,默认值是 Runtime.getRuntime().availableProcessors(),本例采用 1
@Fork
启动多少个子进程分别测试每个被 @Benchmark 标识的方法,本例采用 1
@OutputTimeUnit
统计结果的时间单元,本例是 TimeUnit.MILLISECONDS
@Benchmark
用于标识哪些方法需要被测试
@State
一般而言,性能测试,都会引用一些外部的对象,jmh 要求必须设置外部变量的作用域。可以使用 @State 表示外部对象的作用域。@State 作用于类上,被 @State 标识的对象是在 Thread 范围内还是在 Benchmark。如果是 Thread,则会为每个线程,单独创建对象。如果是 Benchmark 则所有测试共享。
本例的外部变量为 a b c@State 值为 Benchmark
@Setup、@TearDown
2 个注解,均作用于方法上。@Setup 用于测试前的初始化工作; @TearDown 用于回收某些资源
@Param
指定某项参数的多种情况,特别适合用来测试一个函数在不同的参数输入的情况下的性能,只能作用在字段上,使用该注解必须定义 @State 注解。

5、注意事项

为了避免 JIT 优化。因此对于被测试方法,尽量把结果返回。例如以下这段代码,会因为 i 没有被使用,而直接不执行 for 循环

public void add() {
    int i = 12;
    for (int j = 0; j < 12; j++) {
        i += j;
    }
}

正常的代码如下

public int add() {
    int i = 12;
    for (int j = 0; j < 12; j++) {
        i += j;
    }
    return i;
}

imageimage


心无私天地宽
513 声望22 粉丝