基准测试神器 - JMH [ Java Microbenchmark Harness ]
作者:互联网
Table of Contents
一. 简介
JMH,全称 Java Microbenchmark Harness (微基准测试框架),是专门用于Java代码微基准测试的一套测试工具API,是由 OpenJDK/Oracle 官方发布的工具。其精度可以达到毫秒级.可以执行一个函数需要多少时间,或者一个算法有多种不同实现等情况下,选择一个性能最好的那个.
Java的基准测试需要注意的几个点:
- 测试前需要预热。
- 防止无用代码进入测试方法中。
- 并发测试。
- 测试结果呈现。
预热? 为什么要预热? ???????????
因为 JVM 的 JIT 机制的存在,如果某个函数被调用多次之后,JVM 会尝试将其编译成为机器码从而提高执行速度。为了让 benchmark 的结果更加接近真实情况就需要进行预热。
JMH的使用场景:
- 定量分析某个热点函数的优化效果
- 想定量地知道某个函数需要执行多长时间,以及执行时间和输入变量的相关性
- 对比一个函数的多种实现方式
二. 安装 [ idea plug ]
2.1 直接在Plugins的面板搜索 JMH 安装JMH plugin .
2.2 配置idea , 即可.
三. 注解
@Benchmark
@Benchmark标签是用来标记测试方法的,只有被这个注解标记的话,该方法才会参与基准测试,但是有一个基本的原则就是被@Benchmark标记的方法必须是public的。
@Warmup
@Warmup用来配置预热的内容,可用于类或者方法上,越靠近执行方法的地方越准确。一般配置warmup的参数有这些:
-
iterations:预热的次数。
-
time:每次预热的时间。
-
timeUnit:时间单位,默认是s。
-
batchSize:批处理大小,每次操作调用几次方法。(后面用到)
-
@Measurement
用来控制实际执行的内容,配置的选项本warmup一样。
@BenchmarkMode
@BenchmarkMode主要是表示测量的维度,有以下这些维度可供选择:
Mode.Throughput 吞吐量维度
Mode.AverageTime 平均时间
Mode.SampleTime 抽样检测
Mode.SingleShotTime 检测一次调用
Mode.All 运用所有的检测模式 在方法级别指定@BenchmarkMode的时候可以一定指定多个纬度,例如: @BenchmarkMode({Mode.Throughput, Mode.AverageTime, Mode.SampleTime, Mode.SingleShotTime}),代表同时在多个纬度对目标方法进行测量。
@OutputTimeUnit
@OutputTimeUnit代表测量的单位,比如秒级别,毫秒级别,微妙级别等等。一般都使用微妙和毫秒级别的稍微多一点。该注解可以用在方法级别和类级别,当用在类级别的时候会被更加精确的方法级别的注解覆盖,原则就是离目标更近的注解更容易生效。
@State
在很多时候我们需要维护一些状态内容,比如在多线程的时候我们会维护一个共享的状态,这个状态值可能会在每隔线程中都一样,也有可能是每个线程都有自己的状态,JMH为我们提供了状态的支持。该注解只能用来标注在类上,因为类作为一个属性的载体。 @State的状态值主要有以下几种:
- Scope.Benchmark 该状态的意思是会在所有的Benchmark的工作线程中共享变量内容。
- Scope.Group 同一个Group的线程可以享有同样的变量
- Scope.Thread 每隔线程都享有一份变量的副本,线程之间对于变量的修改不会相互影响。
下面看两个常见的@State的写法:
1.直接在内部类中使用@State作为“PropertyHolder”
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 measureUnshared(ThreadState state) {
state.x++;
}
@Benchmark
public void measureShared(BenchmarkState state) {
state.x++;
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JMHSample_03_States.class.getSimpleName())
.threads(4)
.forks(1)
.build();
new Runner(opt).run();
}
}
2.在Main类中直接使用@State作为注解,是Main类直接成为“PropertyHolder”
@State(Scope.Thread)
public class JMHSample_04_DefaultState {
double x = Math.PI;
@Benchmark
public void measure() {
x++;
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JMHSample_04_DefaultState.class.getSimpleName())
.forks(1)
.build();
new Runner(opt).run();
}
}
我们试想以下@State的含义,它主要是方便框架来控制变量的过程逻辑,通过@State标示的类都被用作属性的容器,然后框架可以通过自己的控制来配置不同级别的隔离情况。被@Benchmark标注的方法可以有参数,但是参数必须是被@State注解的,就是为了要控制参数的隔离。
但是有些情况下我们需要对参数进行一些初始化或者释放的操作,就像Spring提供的一些init和destory方法一样,JHM也提供有这样的钩子:
-
@Setup 必须标示在@State注解的类内部,表示初始化操作
-
@TearDown 必须表示在@State注解的类内部,表示销毁操作
-
初始化和销毁的动作都只会执行一次。
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import org.openjdk.jmh.runner.options.Options;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime) // 测试完成时间
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) // 预热 2 轮,每次 1s
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) // 测试 5 轮,每次 3s
@Fork(1) // fork 1 个线程
@State(Scope.Thread) // 每个测试线程一个实例
public class JMHSample_05_StateFixtures {
double x;
@Setup
public void prepare() {
System.out.println("This is Setup ");
x = Math.PI;
}
@TearDown
public void check() {
System.out.println("This is TearDown ");
assert x > Math.PI : "Nothing changed?";
}
@Benchmark
public void measureRight() {
x++;
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JMHSample_05_StateFixtures.class.getSimpleName())
.forks(1)
.jvmArgs("-ea")
.build();
new Runner(opt).run();
}
}
虽然我们可以执行初始化和销毁的动作,但是总是感觉还缺点啥?对,就是初始化的粒度。因为基准测试往往会执行多次,那么能不能保证每次执行方法的时候都初始化一次变量呢? @Setup和@TearDown提供了以下三种纬度的控制:
Level.Trial 只会在个基础测试的前后执行。包括Warmup和Measurement阶段,一共只会执行一次。
Level.Iteration 每次执行记住测试方法的时候都会执行,如果Warmup和Measurement都配置了2次执行的话,那么@Setup和@TearDown配置的方法的执行次数就4次。
Level.Invocation 每个方法执行的前后执行(一般不推荐这么用)
@Param
在很多情况下,我们需要测试不同的参数的不同结果,但是测试的了逻辑又都是一样的,因此如果我们编写镀铬benchmark的话会造成逻辑的冗余,幸好JMH提供了@Param参数来帮助我们处理这个事情,被@Param注解标示的参数组会一次被benchmark消费到。
@State(Scope.Benchmark)
public class ParamTest {
@Param({"1", "2", "3"})
int testNum;
@Benchmark
public String test() {
return String.valueOf(testNum);
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(ParamTest.class.getSimpleName())
.forks(1)
.build();
new Runner(opt).run();
}
}
@Threads
测试线程的数量,可以配置在方法或者类上,代表执行测试的线程数量。
通常看到这里我们会比较迷惑Iteration和Invocation区别,我们在配置Warmup的时候默认的时间是的1s,即1s的执行作为一个Iteration,假设每次方法的执行是100ms的话,那么1个Iteration就代表10个Invocation。
四.使用样例
4.1.修改pom.xml配置文件
<dependencies>
<!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-core -->
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.23</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-generator-annprocess -->
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.23</version>
<scope>provided</scope>
</dependency>
</dependencies>
4.2.测试map的循环输出效率
4.2.1.代码
package com.map;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import org.openjdk.jmh.runner.options.Options;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime) // 测试完成时间
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) // 预热 2 轮,每次 1s
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) // 测试 5 轮,每次1s
@Fork(1) // fork 1 个线程
@State(Scope.Thread) // 每个测试线程一个实例
public class HashMapTest {
static Map<Integer, String> map = new HashMap() {{
// 添加数据
for (int i = 0; i < 10000; i++) {
put(i, "val:" + i);
}
}};
public static void main(String[] args) throws RunnerException {
// 启动基准测试
Options opt = new OptionsBuilder()
.include(HashMapTest.class.getSimpleName()) // 要导入的测试类
// .output("/a/jmh-map.log") // 输出测试结果的文件
.build();
new Runner(opt).run(); // 执行测试
}
@Benchmark
public void entrySet() {
// 遍历
Iterator<Map.Entry<Integer, String>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Integer, String> entry = iterator.next();
Integer k = entry.getKey();
String v = entry.getValue();
}
}
@Benchmark
public void forEachEntrySet() {
// 遍历
for (Map.Entry<Integer, String> entry : map.entrySet()) {
Integer k = entry.getKey();
String v = entry.getValue();
}
}
@Benchmark
public void keySet() {
// 遍历
Iterator<Integer> iterator = map.keySet().iterator();
while (iterator.hasNext()) {
Integer k = iterator.next();
String v = map.get(k);
}
}
@Benchmark
public void forEachKeySet() {
// 遍历
for (Integer key : map.keySet()) {
Integer k = key;
String v = map.get(k);
}
}
@Benchmark
public void lambda() {
// 遍历
map.forEach((key, value) -> {
Integer k = key;
String v = map.get(k);
});
}
@Benchmark
public void streamApi() {
// 单线程遍历
map.entrySet().stream().forEach((entry) -> {
Integer k = entry.getKey();
String v = entry.getValue();
});
}
public void parallelStreamApi() {
// 多线程遍历
map.entrySet().parallelStream().forEach((entry) -> {
Integer k = entry.getKey();
String v = entry.getValue();
});
}
}
4.2.2.结果:
尽量使用 map.entrySet() 进行输出. 因为性能方面照普通的循环快将近2倍.
Benchmark Mode Cnt Score Error Units
HashMapTest.entrySet avgt 5 47831.209 ± 14037.126 ns/op
HashMapTest.forEachEntrySet avgt 5 56189.356 ± 16866.246 ns/op
HashMapTest.forEachKeySet avgt 5 75973.584 ± 7738.707 ns/op
HashMapTest.keySet avgt 5 76458.791 ± 11008.914 ns/op
HashMapTest.lambda avgt 5 80734.554 ± 9695.806 ns/op
HashMapTest.streamApi avgt 5 49939.633 ± 11131.095 ns/op
4.2.3.原因.
EntrySet
之所以比 KeySet
的性能高是因为,KeySet
在循环时使用了 map.get(key)
,而 map.get(key)
相当于又遍历了一遍 Map 集合去查询 key
所对应的值。为什么要用“又”这个词?那是因为在使用迭代器或者 for 循环时,其实已经遍历了一遍 Map 集合了,因此再使用 map.get(key)
查询时,相当于遍历了两遍。
而 EntrySet
只遍历了一遍 Map 集合,之后通过代码“Entry<Integer, String> entry = iterator.next()”把对象的 key
和 value
值都放入到了 Entry
对象中,因此再获取 key
和 value
值时就无需再遍历 Map 集合,只需要从 Entry
对象中取值就可以了。
所以,EntrySet
的性能比 KeySet
的性能高出了一倍,因为 KeySet
相当于循环了两遍 Map 集合,而 EntrySet
只循环了一遍。
4.3.测试list的循环效果
4.3.1.代码
package list;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import org.openjdk.jmh.runner.options.Options;
import java.util.*;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime) // 测试完成时间
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) // 预热 2 轮,每次 1s
@Measurement(iterations = 5, time = 3, timeUnit = TimeUnit.SECONDS) // 测试 5 轮,每次 3s
@Fork(1) // fork 1 个线程
@State(Scope.Thread) // 每个测试线程一个实例
public class ListTest {
static LinkedList<Integer> linkedList = new LinkedList<Integer>(){
{
// 添加数据
for (int i = 0; i < 10000; i++) {
add(i);
}
}
};
static ArrayList<Integer> arrayList = new ArrayList<Integer>(){
{
// 添加数据
for (int i = 0; i < 10000; i++) {
add(i);
}
}
};
public static void main(String[] args) throws RunnerException {
// 启动基准测试
Options opt = new OptionsBuilder()
.include(ListTest.class.getSimpleName()) // 要导入的测试类
// .output("/a/jmh-map.log") // 输出测试结果的文件
.build();
new Runner(opt).run(); // 执行测试
}
@Benchmark
public void linkedListFor() {
// 遍历
for (int i = 0 ; i < linkedList.size() ; i++) {
int x = linkedList.get(i);
}
}
@Benchmark
public void linkedListForEach() {
// 遍历
for (Integer i : linkedList ) {
}
}
@Benchmark
public void arrayListFor() {
// 遍历
for (int i = 0 ; i < arrayList.size() ; i++) {
int x = arrayList.get(i);
}
}
@Benchmark
public void arrayListForEach() {
// 遍历
for (Integer i : arrayList ) {
}
}
}
4.3.2. 结果
Benchmark Mode Cnt Score Error Units
ListTest.arrayListFor avgt 5 7291.515 ± 1348.462 ns/op
ListTest.arrayListForEach avgt 5 6955.242 ± 1587.509 ns/op
ListTest.linkedListFor avgt 5 41467520.339 ± 8443367.361 ns/op
ListTest.linkedListForEach avgt 5 22096.751 ± 6459.100 ns/op
五.官方网站&示例
官网: https://openjdk.java.net/projects/code-tools/jmh/
git hub idea-jmh-plugin : https://github.com/artyushov/idea-jmh-plugin
标签:Java,void,Benchmark,JMH,jmh,Harness,测试,import,public 来源: https://blog.csdn.net/zhanglong_4444/article/details/105948896