性能测试JMH

性能测试JMH

JMH,即(Java Microbenchmark Harness) 用于代码微基准测试的工具套件,主要是基于方法层面的基准测试,精度可以达到纳秒级。 基准测试:是指通过设计科学的测试方法、测试工具和测试系统,实现对一类测试对象的某项性能指标进行定量的和可对比的测试。

  • micro英[ˈmaɪkrəʊ]微观的;

单机压测工具JMH,2013年由oracle内部JIT的大牛们开发,归于OpenJDK

性能测试生成图的网站:http://deepoove.com/jmh-visual-chart/ (偶尔会故障)

注:JDK9后自带JMK

JMH 比较典型的应用场景如下:

  1. 想准确地知道某个方法需要执行多长时间,以及执行时间和输入之间的相关性

  2. 对比接口不同实现在给定条件下的吞吐量

  3. 查看多少百分比的请求在多长时间内完成

JMH入门

maven依赖

<!--Benchmark基准测试-->
<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-core</artifactId>
    <version>1.29</version>
</dependency>
<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-generator-annprocess</artifactId>
    <version>1.29</version>
    <scope>test</scope>
</dependency>

性能测试代码

package org.dh.nicetuantools.service;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
import org.openjdk.jmh.results.format.ResultFormatType;
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.OptionsBuilde
import java.util.concurrent.TimeUnit;
​
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 5, time = 5)
@Threads(4)
@Fork(1)
@State(value = Scope.Benchmark)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class JMHeduTest {
    // 判断 + 和 StringBuilder.append() 两种字符串拼接哪个耗时更短     
    @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());
    }
​
    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(JMHeduTest.class.getSimpleName())
                .result("result.json")
                .resultFormat(ResultFormatType.JSON).build();
        new Runner(opt).run();
    }
}

【说明】:

@Benchmark 注解标识的方法是要测试的方法

main()函数中首先对测试用例进行配置,使用Builder模式配置测试,配置参数存入Options, 并用Options构造Runner启动测试。

测试结果:

# JMH version: 1.29
# VM version: JDK 1.8.0_281, Java HotSpot(TM) 64-Bit Server VM, 25.281-b09
# VM invoker: C:\MySoftwear\Java\jdk1.8\jre\bin\java.exe
# VM options: -javaagent:C:\MySoftwear\Java\ide\IntelliJ IDEA 2020.3.2\lib\idea_rt.jar=53484:C:\MySoftwear\Java\ide\IntelliJ IDEA 2020.3.2\bin -Dfile.encoding=UTF-8
# Blackhole mode: full + dont-inline hint
# Warmup: 3 iterations, 1 s each
# Measurement: 5 iterations, 5 s each
# Timeout: 10 min per iteration
# Threads: 4 threads, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: org.dh.nicetuantools.service.JMHeduTest.testStringAdd
# Parameters: (length = 10)
该部分为测试的基本信息,如java路径,预热代码迭代次数,测试代码迭代次数,使用线程数,测试统计单位
​
# Run progress: 0.00% complete, ETA 00:02:48
# Fork: 1 of 1
# Warmup Iteration   1: 354.206 ±(99.9%) 28.636 ns/op
# Warmup Iteration   2: 283.927 ±(99.9%) 59.079 ns/op
# Warmup Iteration   3: 254.795 ±(99.9%) 50.735 ns/op
该部分为每一次热身中的性能指标,预热测试不会作为最终的统计结果。预热的目的是让 JVM 对被测代码进行足够多的优化,比如,在预热后,被测代码应该得到了充分的 JIT 编译和优化。
​
Iteration   1: 256.655 ±(99.9%) 25.304 ns/op
Iteration   2: 256.456 ±(99.9%) 7.752 ns/op
Iteration   3: 272.966 ±(99.9%) 19.430 ns/op
Iteration   4: 278.945 ±(99.9%) 9.152 ns/op
Iteration   5: 271.370 ±(99.9%) 11.790 ns/op
Result "org.dh.nicetuantools.service.JMHeduTest.testStringAdd":
  267.278 ±(99.9%) 39.229 ns/op [Average]
  (min, avg, max) = (256.456, 267.278, 278.945), stdev = 10.188
  CI (99.9%): [228.049, 306.508] (assumes normal distribution)
​
Benchmark                        (length)  Mode  Cnt      Score     Error  Units
JMHeduTest.testStringAdd              100  avgt    5  10933.988 ± 923.095  ns/op
该部分显示测量迭代的情况,每一次迭代都显示了当前的执行速率,即一个操作所花费的时间。在进行 5 次迭代后,进行统计,在本例中,length 为 100 的情况下 testStringBuilderAdd 方法的平均执行花费时间为 10933.988 ns,误差为 923.095 ns。
​
​
最后的测试结果如下所示:
Benchmark                        (length)  Mode  Cnt      Score     Error  Units
JMHeduTest.testStringAdd               10  avgt    5    267.278 ±  39.229  ns/op
JMHeduTest.testStringAdd               50  avgt    5   2950.072 ±  39.555  ns/op
JMHeduTest.testStringAdd              100  avgt    5  10933.988 ± 923.095  ns/op
JMHeduTest.testStringBuilderAdd        10  avgt    5    175.045 ±  70.243  ns/op
JMHeduTest.testStringBuilderAdd        50  avgt    5   1002.491 ± 671.179  ns/op
JMHeduTest.testStringBuilderAdd       100  avgt    5   2523.618 ± 254.709  ns/op
结果表明,在拼接字符次数越多的情况下,StringBuilder.append() 的性能就更好。

另外一种执行方式在 maven 里增加一个 plugin,具体配置如下:

作者:武培轩
链接:https://www.zhihu.com/question/276455629/answer/1259967560
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
​
<plugins>
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</artifactId>
        <version>2.4.1</version>
        <executions>
            <execution>
                <phase>package</phase>
                <goals>
                    <goal>shade</goal>
                </goals>
                <configuration>
                    <finalName>jmh-demo</finalName>
                    <transformers>
                        <transformer
                                implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                            <mainClass>org.openjdk.jmh.Main</mainClass>
                        </transformer>
                    </transformers>
                </configuration>
            </execution>
        </executions>
    </plugin>
</plugins>

接着执行 maven 的命令生成可执行 jar 包并执行:

mvn clean install
java -jar target/jmh-demo.jar JMHeduTest
Benchmark Mode Cnt Score Error Units
基准测试执行的方法 测试模式 运行多少次 分数 错误 单位

常见注解

@BenchmarkMode用来配置 Mode 选项,可用于类或者方法上,这个注解的 value 是一个数组,可以把几种 Mode 集合在一起执行,如:@BenchmarkMode({Mode.SampleTime, Mode.AverageTime}),还可以设置为 Mode.All,即全部执行一遍。

@Warmup预热所需要配置的一些基本测试参数,可用于类或者方法上。一般前几次进行程序测试的时候都会比较慢,所以要让程序进行几轮预热,保证测试的准确性。 参数如下所示: 1.iterations:预热的次数 2.Time :每次预热的时间 3.timeUnit:时间的单位,默认秒 4.batchSize: 批处理大小,每次操作调用几次方法

@Measurement实际调用方法所需要配置的一些基本测试参数,可用于类或者方法上,参数和 @Warmup 相同。

@Threads每个进程中的测试线程,可用于类或者方法上。

@Fork进行 fork 的次数,可用于类或者方法上。如果 fork 数是 2 的话,则 JMH 会 fork 出两个进程来进行测试。

@State通过 State 可以指定一个对象的作用范围,JMH 根据 scope 来进行实例化和共享操作。@State 可以被继承使用,如果父类定义了该注解,子类则无需定义。由于 JMH 允许多线程同时执行测试,不同的选项含义如下: 1.Scope.Benchmark:所有测试线程共享一个实例,测试有状态实例在多线程共享下的性能 2.Scope.Group:同一个线程在同一个 group 里共享实例 3.Scope.Thread:默认的 State,每个测试线程分配一个实例

@OutputTimeUnit 为统计结果的时间单位,可用于类或者方法注解

@Benchmark:标记需要测试的方法

@Param 指定某项参数的多种情况,特别适合用来测试一个函数在不同的参数输入的情况下的性能,只能作用在字段上,使用该注解必须定义 @State 注解

测试模式(Mode)

@BenchmarkMode注解可选项

1、Throughput: 整体吞吐量, 表示1秒内可以执行多少次调用,单位为 ops/time

2、AverageTime: 调用的平均时间, 每次操作的平均时间,单位为 time/op

3、SampleTime:随机取样,最后输出取样结果的分布,例如“99%的调用在XXX 毫秒以内,99.99%的调用在XXX 毫秒以内”

4、SingleShotTime: 以上模式都是默认一次Iteration是1秒,唯有SingleShotTime 只运行一次。往往同时把warmup 次数设为0, 用于测试冷启动时的性能。

5、All : 上面的所有模式都执行一次

配置类(Options/OptionsBuilder)

使用 Builder 模式配置测试,将配置参数存入 Options 对象,并使用 Options 对象构造 Runner 启动测试。

OptionsBuilder的常用方法及对应的注解形式如下:Options opt = new OptionsBuilder()

启动 new Runner(opt).run();

方法名 参数 作用 对应注解
include 接受一个字符串表达式,表示需要测试的类和方法。 <div style="width:200">指定要运行的基准测试类和方</div> -
exclude 接受一个字符串表达式,表示不需要测试的类和方法 指定不要运行的基准测试类方法 -
warmupIterations 预热的迭代次数 指定预热的迭代次数 @Warmup
warmupBatchSize 预热批量的大小 指定预热批量的大小 @Warmup
warmupForks 预热模式:INDI,BULK,BULK_INDI 指定预热模式 @Warmup
warmupMode 预热的模式 指定预热的模式 @Warmup
warmupTime 预热的时间 指定预热的时间 @Warmup
measurementIterations 测试的迭代次数 指定测试的迭代次数 @Measurement
measurementBatchSize 测试批量的大小 指定测试批量的大小 @Measurement
measurementTime 测试的时间 指定测试的时间 @Measurement
mode 测试模式: Throughput(吞吐量), AverageTime(平均时间),SampleTime(在测试中,随机进行采样执行的时间),SingleShotTime(在每次执行中计算耗时),All(所有) 指定测试模式 @BenchmarkMode--可用于类或者方法上
Fork 子进程数    
threads 每个方法开启线程数量 多线程测试 @Threads,可用在方法或者类上

2.6 其他注解 @OutputTimeUnit   benchmark 结果所使用的时间单位,可用于类或者方法注解,使用java.util.concurrent.TimeUnit中的标准时间单位。 @Setup   方法注解,会在执行 benchmark 之前被执行,正如其名,主要用于初始化。 @TearDown   方法注解,与@Setup相对的,会在所有benchmark执行结束以后执行,主要用于资源的回收等。 @Param   成员注解,可以用来指定某项参数的多种情况。特别适合用来测试一个函数在不同的参数输入的情况下的性能。@Param注解接收一个String数组,在@setup方法执行前转化为为对应的数据类型。多个@Param注解的成员之间是乘积关系,譬如有两个用@Param注解的字段,第一个有5个值,第二个字段有2个值,那么每个测试方法会跑5*2=10次。

注意事项

1、代码死码消除

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

JVM 可能会认为变量 a 从来没有使用过,从而进行优化把整个方法内部代码移除掉,这就会影响测试结果。 JMH 提供了两种方式避免这种问题,一种是将这个变量作为方法返回值 return a,一种是通过 Blackhole 的 consume 来避免 JIT 的优化消除。

其他陷阱还有常量折叠与常量传播、永远不要在测试中写循环、使用 Fork 隔离多个测试方法、方法内联、伪共享与缓存行、分支预测、多线程测试等,感兴趣的可以阅读 https://github.com/lexburner/JMH-samples 了解全部的陷阱。

上一篇:数学建模(三)—— 自动化车床管理


下一篇:NS3-第一个脚本