0%

Java-JMH

JMH

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

JMH结果分析

结果日志解释

  • 基础信息,显示Java路径、Java版本以及JMH基础配置信息
1
2
3
4
5
6
7
8
9
10
11
# JMH version: 1.21
# VM version: JDK 1.8.0_275, OpenJDK 64-Bit Server VM, 25.275-b01
# VM invoker: /Library/Java/JavaVirtualMachines/adoptopenjdk-8.jdk/Contents/Home/jre/bin/java
# VM options: -server
# Warmup: 1 iterations, 10 s each
# Measurement: 1 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 8 threads, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: cn.z201.jmh.ListBenchmark.testArrayList
# Parameters: (size = 100)
  • 预热次数。预热测试不会作为最终的统计结果。预热的目的是让 JVM 对被测代码进行足够多的优化,被测代码应该得到了充分的 JIT 编译和优化。
1
2
3
4
5
# Warmup Iteration   1: 0.002 ±(99.9%) 0.001 ms/op
Iteration 1: 0.002 ±(99.9%) 0.001 ms/op

Result "cn.z201.jmh.ListBenchmark.testArrayList":
0.002 ms/op
  • 结果表明,在拼接字符次数越多的情况下,LinkedList.add() 的性能就更好。这是得益于LinkedList双向链表结构,每次add都是在最后一个位置添加元素。
1
2
3
4
5
6
7
8
9
10
Benchmark                               (size)  Mode  Cnt    Score   Error  Units
ListBenchmark.testArrayList 100 avgt 0.002 ms/op
ListBenchmark.testArrayList 1000 avgt 0.018 ms/op
ListBenchmark.testArrayList 10000 avgt 0.194 ms/op
ListBenchmark.testCopyOnWriteArrayList 100 avgt 0.015 ms/op
ListBenchmark.testCopyOnWriteArrayList 1000 avgt 1.386 ms/op
ListBenchmark.testCopyOnWriteArrayList 10000 avgt 148.161 ms/op
ListBenchmark.testLinkedList 100 avgt 0.002 ms/op
ListBenchmark.testLinkedList 1000 avgt 0.017 ms/op
ListBenchmark.testLinkedList 10000 avgt 0.163 ms/op

注解使用

@BenchmarkMode

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

  • Throughput:整体吞吐量,每秒执行了多少次调用,单位为 ops/time

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

  • SampleTime:随机取样,最后输出取样结果的分布

  • SingleShotTime:只运行一次,往往同时把 Warmup 次数设为 0,用于测试冷启动时的性能

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

@State

通过 State 可以指定一个对象的作用范围,JMH 根据 scope 来进行实例化和共享操作。@State 可以被继承使用,如果父类定义了该注解,子类则无需定义。由于 JMH 允许多线程同时执行测试,不同的选项含义如下:

  • Scope.Benchmark:所有测试线程共享一个实例,测试有状态实例在多线程共享下的性能

  • Scope.Group:同一个线程在同一个 group 里共享实例

  • Scope.Thread:默认的 State,每个测试线程分配一个实例

@OutputTimeUnit

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

@Warmup

预热所需要配置的一些基本测试参数,可用于类或者方法上。一般前几次进行程序测试的时候都会比较慢,所以要让程序进行几轮预热,保证测试的准确性。参数如下所示:

  • iterations:预热的次数

  • time:每次预热的时间

  • timeUnit:时间的单位,默认秒

  • batchSize:批处理大小,每次操作调用几次方法

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

@Measurement

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

@Threads

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

@Fork

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

@Param

  • 指定某项参数的多种情况,特别适合用来测试一个函数在不同的参数输入的情况下的性能,只能作用在字段上,使用该注解必须定义 @State 注解。
  • 如果只是用@Param在编译时会报错,它必须配合@State注解使用,@State指定了对象共享范围。
    • @State(value = Scope.Benchmark):基准测试内共享对象
    • @State(value = Scope.Group):同一个线程组内共享
    • @State(value = Scope.Thread):同一个线程内共享

@Setup & @TearDown

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

案例一

比较 String.join、StringBuilder.append 、StringBuffer.append参数

  • 分别添加100、1000、10000次字符串。

演示代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
package cn.z201.jmh;
/**
* @author z201.coding@gmail.com
**/
@State(Scope.Benchmark)
public class StringBenchmark {
private final static Integer MEASUREMENT_ITERATIONS = 1;
private final static Integer WARMUP_ITERATIONS = 1;

@Param(value = {"100","1000","10000"})
private int size;

public List<Integer> stringList = null;

@Setup(Level.Trial)
public synchronized void initialize() {
try {
stringList = new Random()
.ints()
.limit(size)
.boxed()
.map(i->i.toString())
.collect(Collectors.toList());
} catch (Exception e) {
e.printStackTrace();
}
}

// Throughput 整体吞吐量,例如”1秒内可以执行多少次调用”。
// AverageTime: 调用的平均时间,例如”每次调用平均耗时xxx毫秒”。
// SampleTime: 随机取样,最后输出取样结果的分布,例如”99%的调用在xxx毫秒以内,99.99%的调用在xxx毫秒以内”
// SingleShotTime: 以上模式都是默认一次 iteration 是 1s,唯有 SingleShotTime 是只运行一次。往往同时把 warmup 次数设为0,用于测试冷启动时的性能。
// All(“all”, “All benchmark modes”);
@Test
public void executeJmhRunner() throws RunnerException {
Options opt = new OptionsBuilder()
// 设置类名正则表达式基准为搜索当前类
.include("\\." + StringBenchmark.class.getSimpleName() + "\\.")
// 都是一些基本的参数,可以根据具体情况调整。一般比较重的东西可以进行大量的测试,放到服务器上运行。
.warmupIterations(WARMUP_ITERATIONS) // 多少次预热
.measurementIterations(MEASUREMENT_ITERATIONS) // 要做多少次测量m
.timeUnit(TimeUnit.MILLISECONDS) // 毫秒
// 不使用多线程
.forks(0) // 进行 fork 的次数。如果 fork 数是2的话,则 JMH 会 fork 出两个进程来进行测试。
.threads(8) // 每个进程中的测试线程,这个非常好理解,根据具体情况选择,一般为cpu乘以2。
.mode(Mode.AverageTime)
.shouldDoGC(true)
.shouldFailOnError(true)
.resultFormat(ResultFormatType.JSON) // 输出格式化
// .result("/dev/null") // set this to a valid filename if you want reports
.result("benchmark.json")
.shouldFailOnError(true)
.jvmArgs("-server")
.build();
new Runner(opt).run();
}

@Benchmark
public void testStringJoin(Blackhole blackhole){
String str = new String();
stringList.stream().forEach(i-> str.join(i.toString()));
blackhole.consume(str);
}

@Benchmark
public void testStringBuffer(Blackhole blackhole){
StringBuffer stringBuffer = new StringBuffer();
stringList.stream().forEach(i-> stringBuffer.append(i.toString()));
blackhole.consume(stringBuffer);
}

@Benchmark
public void testStringBuilder(Blackhole blackhole){
StringBuilder stringBuilder = new StringBuilder();
stringList.stream().forEach(i-> stringBuilder.append(i.toString()));
blackhole.consume(stringBuilder);
}
}

案例二

比较ArrayList.add 、LinkedList.add、CopyOnWriteArrayList.add

  • 分别添加100、1000、10000次

演示代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
/**
* @author z201.coding@gmail.com
**/
@State(Scope.Benchmark)
public class ListBenchmark {

private final static Integer MEASUREMENT_ITERATIONS = 1;
private final static Integer WARMUP_ITERATIONS = 1;

@Param(value = {"100","1000","10000"})
private int size;

public List<String> stringList = null;

@Setup(Level.Trial)
public synchronized void initialize() {
try {
stringList = new Random()
.ints()
.limit(size)
.boxed()
.map(i->i.toString())
.collect(Collectors.toList());
} catch (Exception e) {
e.printStackTrace();
}
}

// Throughput 整体吞吐量,例如”1秒内可以执行多少次调用”。
// AverageTime: 调用的平均时间,例如”每次调用平均耗时xxx毫秒”。
// SampleTime: 随机取样,最后输出取样结果的分布,例如”99%的调用在xxx毫秒以内,99.99%的调用在xxx毫秒以内”
// SingleShotTime: 以上模式都是默认一次 iteration 是 1s,唯有 SingleShotTime 是只运行一次。往往同时把 warmup 次数设为0,用于测试冷启动时的性能。
// All(“all”, “All benchmark modes”);
@Test
public void executeJmhRunner() throws RunnerException {
Options opt = new OptionsBuilder()
// 设置类名正则表达式基准为搜索当前类
.include("\\." + getClass().getSimpleName() + "\\.")
// 都是一些基本的参数,可以根据具体情况调整。一般比较重的东西可以进行大量的测试,放到服务器上运行。
.warmupIterations(WARMUP_ITERATIONS) // 多少次预热
.measurementIterations(MEASUREMENT_ITERATIONS) // 要做多少次测量m
.timeUnit(TimeUnit.MILLISECONDS) // 毫秒
// 不使用多线程
.forks(0) // 进行 fork 的次数。如果 fork 数是2的话,则 JMH 会 fork 出两个进程来进行测试。
.threads(8) // 每个进程中的测试线程,这个非常好理解,根据具体情况选择,一般为cpu乘以2。
.mode(Mode.AverageTime)
.shouldDoGC(true)
.shouldFailOnError(true)
.resultFormat(ResultFormatType.JSON) // 输出格式化
// .result("/dev/null") // set this to a valid filename if you want reports
.result("ListBenchmark.json")
.shouldFailOnError(true)
.jvmArgs("-server")
.build();
new Runner(opt).run();
}

@Benchmark
public void testArrayList(Blackhole blackhole){
List<String> arrayList = new ArrayList<>();
stringList.stream().forEach(i->arrayList.add(i));
blackhole.consume(arrayList);
}

@Benchmark
public void testLinkedList(Blackhole blackhole){
List<String> linkedList = new LinkedList<>();
stringList.stream().forEach(i->linkedList.add(i));
blackhole.consume(linkedList);
}

@Benchmark
public void testCopyOnWriteArrayList(Blackhole blackhole){
List<String> copyOnWriteArrayList = new CopyOnWriteArrayList<>();
stringList.stream().forEach(i->copyOnWriteArrayList.add(i));
blackhole.consume(copyOnWriteArrayList);
}
}

案例三

JMH 官方提供了生成 jar 包的方式来执行

  • 对比AtomicLong 、VolatileLong 、log求和

打包项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<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</finalName>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>cn.z201.jmh.Main</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85

/**
* @author z201.coding@gmail.com
**/
@State(Scope.Benchmark)
public class Main {

private final static Integer MEASUREMENT_ITERATIONS = 1;
private final static Integer WARMUP_ITERATIONS = 1;

@Param(value = {"100", "1000", "10000"})
private int size;

public List<Integer> numberList = null;

@Setup(Level.Trial)
public synchronized void initialize() {
try {
numberList = new Random()
.ints()
.limit(size)
.boxed()
.collect(Collectors.toList());
} catch (Exception e) {
e.printStackTrace();
}
}

public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
// 设置类名正则表达式基准为搜索当前类
.include("\\." + Main.class.getSimpleName() + "\\.")
// 都是一些基本的参数,可以根据具体情况调整。一般比较重的东西可以进行大量的测试,放到服务器上运行。
.warmupIterations(WARMUP_ITERATIONS) // 多少次预热
.measurementIterations(MEASUREMENT_ITERATIONS) // 要做多少次测量m
.timeUnit(TimeUnit.MILLISECONDS) // 毫秒
// 不使用多线程
.forks(0) // 进行 fork 的次数。如果 fork 数是2的话,则 JMH 会 fork 出两个进程来进行测试。
.threads(8) // 每个进程中的测试线程,这个非常好理解,根据具体情况选择,一般为cpu乘以2。
.mode(Mode.AverageTime)
.shouldDoGC(true)
.shouldFailOnError(true)
.resultFormat(ResultFormatType.JSON) // 输出格式化
// .result("/dev/null") // set this to a valid filename if you want reports
.result("Main.json")
.shouldFailOnError(true)
.jvmArgs("-server")
.build();
new Runner(opt).run();
}

public static class VolatileLong {
private volatile long value = 0;

public synchronized void add(long amount) {
this.value += amount;
}

public long getValue() {
return this.value;
}
}

@Benchmark
public void atomicLong(Blackhole blackhole) {
AtomicLong atomicLong = new AtomicLong();
numberList.parallelStream().forEach(atomicLong::addAndGet);
blackhole.consume(atomicLong.get());
}

@Benchmark
public void volatileLong(Blackhole blackHole) {
VolatileLong volatileLong = new VolatileLong();
numberList.parallelStream().forEach(volatileLong::add);
blackHole.consume(volatileLong.getValue());
}

@Benchmark
public void longStreamSum(Blackhole blackHole) {
long sum = numberList.parallelStream().mapToLong(s -> s).sum();
blackHole.consume(sum);
}

}

启动

1
2
➜  JMH git:(main) ✗ cd target 
➜ target git:(main) ✗ java -jar jmh.jar

END