文章目录
楔子
学习《Java高并发编程详解——深入理解核心并发库》
1.1 JMH简介
专门用于代码微基准测试的工具集。
官网地址:http://openjdk.java.net/projects/code-tools/jmh/
import com.google.common.base.Stopwatch;
import com.google.common.base.Strings;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* @author MI
* @version 1.0
* @date 2021/5/15 19:13
*/
public class ArrayListVSLinkedList {
private final static String DATA = "dummy DATA";
private final static int MAX_CAPCITY = 1_000_000;
private final static int MAX_ITERATIONS = 10;
private static void test(List<String> list) {
for (int i = 0; i < MAX_CAPCITY; i++) {
list.add(DATA);
}
}
private static void arrayListPerfTest(int iterations) {
for (int i = 0; i < iterations; i++) {
final ArrayList<String> list = new ArrayList<>();
final Stopwatch stopwatch = Stopwatch.createStarted();
test(list);
System.out.println(stopwatch.stop().elapsed(TimeUnit.MILLISECONDS));
}
}
private static void linkedListPerfTest(int iterations) {
for (int i = 0; i < iterations; i++) {
final LinkedList<String> list = new LinkedList<>();
final Stopwatch stopwatch = Stopwatch.createStarted();
test(list);
System.out.println(stopwatch.stop().elapsed(TimeUnit.MILLISECONDS));
}
}
public static void main(String[] args) {
arrayListPerfTest(MAX_ITERATIONS);
System.out.println(Strings.repeat("#", 10));
linkedListPerfTest(MAX_ITERATIONS);
}
}
1.2.2 用JMH进行微基准测试
使用的是JMH 1.19版本
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.19</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.19</version>
</dependency>
import org.openjdk.jmh.annotations.*;
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.OptionsBuilder;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* @author MI
* @version 1.0
* @date 2021/5/15 19:46
*/
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Thread)
public class JMHExample01 {
private final static String DATA = "DUMMY DATA";
private List<String> arrayList;
private List<String> linkedList;
@Setup(Level.Iteration)
public void setUp() {
this.arrayList = new ArrayList<>();
this.linkedList = new LinkedList<>();
}
@Benchmark
public List<String> arrayListAdd() {
this.arrayList.add(DATA);
return arrayList;
}
@Benchmark
public List<String> linkedListAdd() {
this.linkedList.add(DATA);
return linkedList;
}
public static void main(String[] args) throws RunnerException {
final Options options = new OptionsBuilder().include(JMHExample01.class.getSimpleName())
.forks(1)
.measurementIterations(10)
.warmupIterations(10)
.build();
new Runner(options).run();
}
}
测试结果
# Run complete. Total time: 00:01:07
Benchmark Mode Cnt Score Error Units
JMHExample01.arrayListAdd avgt 10 0.087 ± 0.137 us/op
JMHExample01.linkedListAdd avgt 10 0.143 ± 0.027 us/op
虽然输出的信息很多,但目前只需要看最后两行,大体上可以发现arrayListAdd方法调用的平均响应时间为0.087微妙,误差在0.137微妙,效果好于linkedListAdd
1.3 JHM的基本用法
1.3.1 @Benchmark 标记基准测试方法
如果一个类没有任何基准测试方法,那么对其进行基准测试则会出现异常。
import org.openjdk.jmh.annotations.*;
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.OptionsBuilder;
import java.util.concurrent.TimeUnit;
/**
* @author MI
* @version 1.0
* @date 2021/5/15 19:46
*/
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Thread)
public class JMHExample02 {
public void normalMethod() {
}
public static void main(String[] args) throws RunnerException {
final Options options = new OptionsBuilder().include(JMHExample02.class.getSimpleName())
.forks(1)
.measurementIterations(10)
.warmupIterations(10)
.build();
new Runner(options).run();
}
}
Exception in thread "main" No benchmarks to run; check the include/exclude regexps.
1.3.2 warmup以及 measurement
warmup以及measurement ,朱亚市分批次执行基准测试方法。这每个批次中,调动基准测试方法的次数受到两个因素的响应,第一,要根据相关的参数进行设置,第二是根据该方法具体的CPU时间而定,但是通常情况下,我们可以更多关注批次数量即可。
warmup可直译为“预热”的意思,在JMH中,warmup所做的就是在基准测试代码正式度量之前,先对其进行预热,使得代码的执行是经历过了类的早期优化、JVM运行期编译、JIT优化之后的最终状态,从而使得获得代码真实的性能数据。measurement则是真正的度量操作,在每一轮的度量中,所有的度量数据都会被纳入统计之中(预热的数据不会纳入统计之中)
1 设置全局的warmup以及 measurement的执行批次
final Options opts = new OptionsBuilder().include(JMHExample02.class.getSimpleName())
.forks(1)
.measurementIterations(5)//度量的执行批次为5,这5个批次的 ,将会纳入统计
.warmupIterations(3)// 在真正的度量之前,首先会对代码进行3个批次的热身,使代码的运行达到JVM已经优化的效果
.build();
new Runner(opts).run();
除了构造Options时设置warmup以及measurement,还可以使用注解的方式设置
2 使用@Measurement @Warmup注解进行设置
除了使用全局参数的方式之外,还可以在方法上设置对应的基准测试方法的批次参数。
import org.openjdk.jmh.annotations.*;
import java.util.concurrent.TimeUnit;
/**
* @author MI
* @version 1.0
* @date 2021/5/15 19:46
*/
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Thread)
@Measurement(iterations = 5)
@Warmup(iterations = 2)
public class JMHExample03 {
@Benchmark
public void test() throws InterruptedException {
TimeUnit.MILLISECONDS.sleep(1);
}
/**
* 预热5个批次
* 度量10个批次
*/
@Measurement(iterations = 10)
@Warmup(iterations = 5)
@Benchmark
public void test2() throws InterruptedException {
TimeUnit.MILLISECONDS.sleep(1);
}
}
test基准方法执行了2个批次的预热和5个批次的度量,而test2方法则执行了10个批次的度量和5个批次的预热操作。也就是test2通过注解的方式覆盖了全局的设置。
笔者经过测试发现,通过注解的方式设置的全局Measurement Warmup参数是可以被基准测试方法通过同样的方式覆盖的,但是通过Options进行的全局设置则无法被覆盖,也就是说,通过Options设置的参数会应用于所有的基准测试方法且无法被修改(当然不同的版本可能存在差异)
3 Measurement Warmup执行相关的输出
选取上面test输出的分析
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Thread)
@Fork(1)
@Measurement(iterations = 5)
@Warmup(iterations = 2)
public class JMHExample04 {
@Benchmark
public void test() throws InterruptedException {
TimeUnit.MILLISECONDS.sleep(1);
}
}
# 使用的JMH版本是 1.19
# JMH version: 1.19
# JMH的版本信息
# VM version: JDK 9.0.1, VM 9.0.1+11
# Java命令的目录
# VM invoker: C:\soft\a\jdk-9.0.1\bin\java.exe
# JVM运行时指定的参数
# VM options: -javaagent:C:\soft\ide\JetBrains\IntelliJ IDEA 2020.2\lib\idea_rt.jar=14918:C:\soft\ide\JetBrains\IntelliJ IDEA 2020.2\bin -Dfile.encoding=UTF-8
# 热身的批次为2 每个批次将不断地调用test方法,每一个批次执行的时间均为1秒
# Warmup: 2 iterations, 1 s each
# 真正度量的批次为5 ,这5个批次的调用产生的性能数据才会正真的纳入统计中,同样每一个批次的度量执行时间为1秒
# Measurement: 5 iterations, 1 s each
# 每一个批次的超时时间
# Timeout: 10 min per iteration
# 执行基准批次的线程数量
# Threads: 1 thread, will synchronize iterations
# Benchmark mode,这里表明统计的是方法调用一次所耗费的单位时间
# Benchmark mode: Average time, time/op
# Benchmark 方法的绝对路径
# Benchmark: com.study.wwj.api.char01.JMHExample04.test
# 执行进度
# Run progress: 0.00% complete, ETA 00:00:07
# Fork: 1 of 1
# 执行两个批次的热身,第一批次的调用方法平均耗时为1538us,第二批次调用平均耗时为1377us
# Warmup Iteration 1: 1538.110 us/op
# Warmup Iteration 2: 1377.866 us/op
# 执行5个批次的度量
Iteration 1: 1389.788 us/op
Iteration 2: 1392.904 us/op
Iteration 3: 1385.853 us/op
Iteration 4: 1437.593 us/op
Iteration 5: 1408.419 us/op
# 最终统计结果
Result "com.study.wwj.api.char01.JMHExample04.test":
1402.911 ±(99.9%) 81.605 us/op [Average]
#最小值 平均值 最大及以及标准差
(min, avg, max) = (1385.853, 1402.911, 1437.593), stdev = 21.193
CI (99.9%): [1321.306, 1484.517] (assumes normal distribution)
# Run complete. Total time: 00:00:08
Benchmark Mode Cnt Score Error Units
JMHExample04.test avgt 5 1402.911 ± 81.605 us/op
1.3.3 四大BenchmarkMode
JMH使用BenchmarkMode这个注解来声明使用哪一种模式来运行,JMH为我们提供了四种模式,当然他还运行若干个模式同时存在。Mode无非就是统计基准测试数据的不同方式和维度口径。
1 AverageTime
平均响应时间。
@Measurement(iterations = 5)
@BenchmarkMode(Mode.AverageTime)
@Benchmark
public void test() throws InterruptedException {
TimeUnit.MILLISECONDS.sleep(1);
}
Benchmark Mode Cnt Score Error Units
JMHExample04.test avgt 5 1377.904 ± 79.010 us/op
平均执行耗时1377us。
2 Throughput
Throughput 方法吞吐,输出的信息表明了在单位时间内可以对该方法调用多少次
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Measurement(iterations = 5)
@BenchmarkMode(Mode.Throughput)
@Benchmark
public void testThroughput() throws InterruptedException {
TimeUnit.MILLISECONDS.sleep(1);
}
Benchmark Mode Cnt Score Error Units
JMHExample04.testThroughput thrpt 5 0.648 ± 0.139 ops/ms
在1毫秒内,testThroughput方法只会被调用0.648次
3 SampleTime
时间采样的方式是采用一个钟抽样的方式来统计基准测试方法的性能结果。与我们常见的直方图几乎是一样的,它会收集所有的性能数据,并且将其分布在不同的区间中。
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Measurement(iterations = 5)
@BenchmarkMode(Mode.SampleTime)
@Benchmark
public void testSampleTime() throws InterruptedException {
TimeUnit.MILLISECONDS.sleep(1);
}
Histogram, ms/op:
[0.000, 0.500) = 2
[0.500, 1.000) = 144
[1.000, 1.500) = 454
[1.500, 2.000) = 1472
[2.000, 2.500) = 709
[2.500, 3.000) = 8
[3.000, 3.500) = 2
[3.500, 4.000) = 0
[4.000, 4.500) = 2
[4.500, 5.000) = 0
[5.000, 5.500) = 2
[5.500, 6.000) = 0
[6.000, 6.500) = 0
[6.500, 7.000) = 0
[7.000, 7.500) = 0
[7.500, 8.000) = 0
[8.000, 8.500) = 1
Percentiles, ms/op:
p(0.0000) = 0.287 ms/op
p(50.0000) = 1.970 ms/op
p(90.0000) = 2.038 ms/op
p(95.0000) = 2.106 ms/op
p(99.0000) = 2.314 ms/op
p(99.9000) = 5.280 ms/op
p(99.9900) = 8.389 ms/op
p(99.9990) = 8.389 ms/op
p(99.9999) = 8.389 ms/op
p(100.0000) = 8.389 ms/op
# Run complete. Total time: 00:00:08
Benchmark Mode Cnt Score Error Units
JMHExample04.testSampleTime sample 2796 1.787 ± 0.027 ms/op
JMHExample04.testSampleTime:testSampleTime·p0.00 sample 0.287 ms/op
JMHExample04.testSampleTime:testSampleTime·p0.50 sample 1.970 ms/op
JMHExample04.testSampleTime:testSampleTime·p0.90 sample 2.038 ms/op
JMHExample04.testSampleTime:testSampleTime·p0.95 sample 2.106 ms/op
JMHExample04.testSampleTime:testSampleTime·p0.99 sample 2.314 ms/op
JMHExample04.testSampleTime:testSampleTime·p0.999 sample 5.280 ms/op
JMHExample04.testSampleTime:testSampleTime·p0.9999 sample 8.389 ms/op
JMHExample04.testSampleTime:testSampleTime·p1.00 sample 8.389 ms/op
总共进行了2796调用,该方法的平均响应时间为1.787毫秒,有2次落在了 0~0.5毫秒
4 SingleShotTime
主要用来进行冷测试,无论Measurement还是Warmup,在每一次中基准测试方法只会被执行一次。一般情况下,我们将Warmup的批次设置为0.
1.3.4 OutputTimeUnit
提供了统计结果输出时的单位,比如,调用一次该方法将会耗时多少个单位时间,或者在单位时间内对该方法进行了多少次调用。
1.3.5 三大State的使用
在JMH中,有三大State分别对应于Scope的三个枚举值
- Thread
- Group
- Benchmark
**
1 Thread独享的State
所谓线程独享的State是指,每一个运行基准测试方法的线程都会持有一个独立的对象实例,该实例既可能是测试方法参数传入的,也可能是运行基准方法所在的宿主class。将State设置为Scope.Thread一般主要是针对非线程安全的类。
import org.openjdk.jmh.annotations.*;
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.OptionsBuilder;
import java.util.concurrent.TimeUnit;
/**
* @author MI
* @version 1.0
* @date 2021/5/15 19:46
*/
@BenchmarkMode(Mode.AverageTime)
@Fork(1)
@Warmup(iterations = 5)
@Measurement(iterations = 10)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
//设置5个线程运行基准测试方法
@Threads(5)
public class JMHExample06 {
//5个线程,每一个线程都会持有一个test实例
@State(Scope.Thread)
public static class Test {
public Test() {
System.out.println("create instance");
}
public void method() {
}
}
// 通过基准测试将state引用传入
@Benchmark
public void test(Test test) {
test.method();
}
public static void main(String[] args) throws RunnerException {
final Options opts = new OptionsBuilder().include(JMHExample06.class.getSimpleName())
.build();
new Runner(opts).run();
}
}
2 Thread共享的State
测试在多线程的情况下某个类被不同线程操作时的性能,比如多线程访问某个共享数据时,我们需要让多个线程使用同一个实例才可以,因此提供了多线程共享的一种状态
@BenchmarkMode(Mode.AverageTime)
@Fork(1)
@Warmup(iterations = 5)
@Measurement(iterations = 10)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
//设置5个线程运行基准测试方法
@Threads(5)
public class JMHExample07 {
//5个线程, test实例会被多个线程共享,也就是说只有一份test的实例
@State(Scope.Benchmark)
public static class Test {
public Test() {
System.out.println("create instance");
}
public void method() {
}
}
// 通过基准测试将state引用传入
@Benchmark
public void test(Test test) {
test.method();
}
public static void main(String[] args) throws RunnerException {
final Options opts = new OptionsBuilder().include(JMHExample07.class.getSimpleName())
.build();
new Runner(opts).run();
}
}
3 线程组共享的State
截止目前,上述所有的基准测试方法都会被JMH根据方法名的字典顺序排序后按照顺序逐个地调用执行,因此不存在两个方法同时运行的情况,如果想测试某个共享数据或共享资源在多线程的情况下同时被读写的行为,是没有办法进行的,比如在,在多线程高并发的环境中,多个线程同时对一个ConcurrentHashMap进行读写。
import org.openjdk.jmh.annotations.*;
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.OptionsBuilder;
import java.util.concurrent.TimeUnit;
/**
* @author MI
* @version 1.0
* @date 2021/5/15 19:46
*/
@BenchmarkMode(Mode.AverageTime)
@Fork(1)
@Warmup(iterations = 5)
@Measurement(iterations = 10)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class JMHExample08 {
// 设置为线程组共享
@State(Scope.Group)
public static class Test {
public Test() {
System.out.println("create instance");
}
public void write() {
System.out.println("write");
}
public void read() {
System.out.println("read");
}
}
// 在线程组test中,有三个线程将不断地对test实例的write方法进行调用
@GroupThreads(3)
@Group("test")
@Benchmark
public void testwrite(Test test) {
test.write();
}
// 在线程组test中,有三个线程将不断地对test实例的write方法进行调用
@GroupThreads(3)
@Group("test")
@Benchmark
public void testRead(Test test) {
test.read();
}
public static void main(String[] args) throws RunnerException {
final Options opts = new OptionsBuilder().include(JMHExample08.class.getSimpleName())
.build();
new Runner(opts).run();
}
}
# 总共6个线程会执行基准测试方法,这6个线程都在同一个group中,testRead 方法被3个线程执行
# Threads: 6 threads (1 group; 3x "testRead", 3x "testwrite" in each group), will synchronize iterations
# Benchmark mode: Average time, time/op
1.3.6 @Param的妙用
1 对比ConcurrentHashMap和SynchronizedMap的性能
@BenchmarkMode(Mode.AverageTime)
@Fork(1)
@Warmup(iterations = 5)
@Measurement(iterations = 10)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
//5个线程同时对资源进行测试
@Threads(5)
//设置为线程间共享的资源
@State(Scope.Benchmark)
public class JMHExample09 {
private Map<Long, Long> concurrentMap;
private Map<Long, Long> syschronizedMap;
@Setup
public void setup() {
concurrentMap = new ConcurrentHashMap<>();
syschronizedMap = Collections.synchronizedMap(new HashMap<>());
}
@Benchmark
public void testconcurrentMap() {
this.concurrentMap.put(System.nanoTime(), System.nanoTime());
}
@Benchmark
public void testsyschronizedMap() {
this.syschronizedMap.put(System.nanoTime(), System.nanoTime());
}
public static void main(String[] args) throws RunnerException {
final Options opts = new OptionsBuilder().include(JMHExample09.class.getSimpleName())
.build();
new Runner(opts).run();
}
}
通过基准测试,不难发现,ConcurrentHashMap比SynchronizedMap的表现要优秀很多(多线程使用put)
2 使用@Param
Java提供的具备喜爱昵称安全的Map接口实现并非只有oncurrentHashMap和_synchronizedMap_ ,同样ConcurrentSkipListMap和HashTable也可提供选择,如果要对其进行测试。很显然,这种方式存在大量的冗余代码,因此JMH为我们提供了@Param注解,它使得参数可配置,也就是说一个参数在每一次的基准测试是都会有不同的值与之对应。
package com.study.wwj.api.char01;
import org.openjdk.jmh.annotations.*;
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.OptionsBuilder;
import java.util.Collections;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.TimeUnit;
/**
* @author MI
* @version 1.0
* @date 2021/5/15 19:46
*/
@BenchmarkMode(Mode.AverageTime)
@Fork(1)
@Warmup(iterations = 5)
@Measurement(iterations = 5)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
//5个线程同时对资源进行测试
@Threads(5)
//设置为线程间共享的资源
@State(Scope.Benchmark)
public class JMHExample010 {
//为type 提供了 四种可配置爱的参数值
@Param({"1", "2", "3", "4"})
private int type;
private Map<Long, Long> map;
@Setup
public void setup() {
switch (type) {
case 1:
this.map = new ConcurrentHashMap<>();
break;
case 2:
this.map = new ConcurrentSkipListMap<>();
break;
case 3:
this.map = new Hashtable<>();
break;
case 4:
this.map = Collections.synchronizedMap(new HashMap<>());
break;
default:
throw new IllegalArgumentException("Illegal map type");
}
}
//只需要一个基准方法即可
@Benchmark
public void test() {
this.map.put(System.nanoTime(), System.nanoTime());
}
public static void main(String[] args) throws RunnerException {
final Options opts = new OptionsBuilder().include(JMHExample010.class.getSimpleName())
.build();
new Runner(opts).run();
}
}
1.3.7 JMH 的测试套件(Fixture)
1 Setup以及TearDown
Setup会在每一个基准测试方法执行前被调用,通常用于资源的初始化。TearDown则会在基准测试方法被执行之后调用,通常可以用于回收清理工作。
import org.openjdk.jmh.annotations.*;
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.OptionsBuilder;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* @author MI
* @version 1.0
* @date 2021/5/15 19:46
*/
@BenchmarkMode(Mode.AverageTime)
@Fork(1)
@Warmup(iterations = 5)
@Measurement(iterations = 5)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
//设置为线程间共享的资源
@State(Scope.Benchmark)
public class JMHExample011 {
private List<String> list;
//将方法标记为setup,执行初始化操作
@Setup
public void setup() {
this.list = new ArrayList<>();
}
@Benchmark
public void measureRight() {
this.list.add("test");
}
@Benchmark
public void measureWrong() {
//do nothing
}
//运行资源回收甚至断言的操作
@TearDown
public void tearDown() {
assert this.list.size() > 0 : "the list elements must greatr than zero";
}
public static void main(String[] args) throws RunnerException {
final Options opts = new OptionsBuilder().include(JMHExample011.class.getSimpleName())
.jvmArgs("-ea")//激活断言 enable assertion的意思
.build();
new Runner(opts).run();
}
}
2 Level
上述使用Setup和TearDown时,在默认你情况下,SetUp和TearDown会在一个基准方法的所有批次执行前后分别执行,如果需要每一个批次或者每一次基准方法调用的前后执行对应的套件方法,则需要对Setup和TearDown进行简单的配置。
- @Setup**(**Level.**Trial) **给套件方法会在每一个基准测试方法的所有批次执行的前后被执行。
- @Setup**(**Level.Iteration) 如果想要在每一个基准测试执行的前后调用套件方法,则可以设置此。
- @Setup**(**Level.**Invocation) **设置为此意味着在每一个批次的度量过程中,每一对基准方法的调用前后都会执行套件方法。
需要注意的是,套件的执行也会产生CPU时间的消耗,但是JMH并不会将这部分时间纳入基准测试方法的统计之中。
1.3.8 CompilerControl
import org.openjdk.jmh.annotations.*;
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.OptionsBuilder;
import java.util.concurrent.TimeUnit;
/**
* @author MI
* @version 1.0
* @date 2021/5/15 19:46
*/
@BenchmarkMode(Mode.AverageTime)
@Fork(1)
@Warmup(iterations = 5)
@Measurement(iterations = 5)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
//设置为线程间共享的资源
@State(Scope.Thread)
public class JMHExample012 {
@Benchmark
public void test1() {
}
@Benchmark
public void test2() {
Math.log(Math.PI);
}
public static void main(String[] args) throws RunnerException {
final Options opts = new OptionsBuilder().include(JMHExample012.class.getSimpleName())
.build();
new Runner(opts).run();
}
}
Benchmark Mode Cnt Score Error Units
JMHExample012.test1 avgt 5 ≈ 10⁻⁴ us/op
JMHExample012.test2 avgt 5 0.008 ± 0.001 us/op
禁止优化
//禁止优化
@CompilerControl(CompilerControl.Mode.EXCLUDE)
@Benchmark
public void test1() {
}
@CompilerControl(CompilerControl.Mode.EXCLUDE)
@Benchmark
public void test2() {
Math.log(Math.PI);
}
Benchmark Mode Cnt Score Error Units
JMHExample012.test1 avgt 5 0.014 ± 0.003 us/op
JMHExample012.test2 avgt 5 0.032 ± 0.005 us/op
JVM在运行test2方法时对我们的程序进行了优化,具体来说就是将log运输的相关代码进行了运行期的擦除。
如果想在自己的应用程序中杜绝JVM运行期的优化,那么可以采用如下方式实现。
- 通过编写程序的方式禁止JVM运行期动态编译和优化java.lang.Compiler.disable()
- 在启动JVM时增加参数-Djava.compiler=NONE。
1.4 编写正确的微基准测试以及高级用法
虽然JMH可以帮助我们更好地了解我们所编写的代码,但是如果我们所编写的JMH基准测试方法本身就有问题,那么就会很难起到指导作用,甚至还会产生误差
1.4.1 编写正确的微基准测试用例
1 避免DCE(Dead Code Elimination)
所谓DCE 是指JVM为我们擦去一些上下文无关,甚至经过计算之后压根不会用到的代码,比如
pulic void test(){
int x=0;
int y =0;
int z=x+y;
}
虽然在test方法中定义了x和y,并且经过了相加运算得到了z,但是该方法的上下文中没有其他地方用到z,JVM很可能会将test方法当做一个空的方法来看待。
import org.openjdk.jmh.annotations.*;
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.OptionsBuilder;
import java.util.concurrent.TimeUnit;
/**
* @author MI
* @version 1.0
* @date 2021/5/15 19:46
*/
@BenchmarkMode(Mode.AverageTime)
@Fork(1)
@Warmup(iterations = 5)
@Measurement(iterations = 5)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
//设置为线程间共享的资源
@State(Scope.Thread)
public class JMHExample013 {
/**
* 空方法,主要用于做基准数据
*/
@Benchmark
public void baseline() {
}
/**
* 虽然进行了log运算,但是结果既没有再进行二次使用,没有返回
*/
@Benchmark
public void measureLog1() {
//进行数学运算,但是在局部方法内
Math.log(Math.PI);
}
/**
* 第二次的结果没有更进一步的使用
*/
@Benchmark
public void measureLog2() {
double result = Math.log(Math.PI);
Math.log(result);
}
/**
* 结果进行了返回操作
*
* @return
*/
@Benchmark
public double measureLog3() {
return Math.log(Math.PI);
}
public static void main(String[] args) throws RunnerException {
final Options opts = new OptionsBuilder().include(JMHExample013.class.getSimpleName())
.build();
new Runner(opts).run();
}
}
若想要编写性能良好的微基准测试方法,则不要让方法存在Dead Code。最好每一个基准测试方法都有返回值。
2 使用Blackhole
假设在基准测试方法中,需要将两个计算结果作为返回值,那么我们该如何去做呢?我们第一时间想到的可能是将结果存放到某个数组或者容器当中作为返回值,但是这种对数组或容器的操作会对性能统计造成干扰,因为对数据或者容器的写操作也是需要花费一定的CPU时间的。
JMH提供了一个称为Blackhole的类。可以在不作任何返回的情况下避免Dead Code的发生。
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
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.OptionsBuilder;
import java.util.concurrent.TimeUnit;
/**
* @author MI
* @version 1.0
* @date 2021/5/15 19:46
*/
@BenchmarkMode(Mode.AverageTime)
@Fork(1)
@Warmup(iterations = 5)
@Measurement(iterations = 5)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
//设置为线程间共享的资源
@State(Scope.Thread)
public class JMHExample014 {
double x1 = Math.PI;
double x2 = Math.PI * 2;
@Benchmark
public double baseline() {
return Math.pow(x1, 2);
}
@Benchmark
public double powButReturnOne() {
//Dead Code 会被擦除
Math.pow(x1, 2);
//不会别擦除,因为返回结果
return Math.pow(x2, 2);
}
@Benchmark
public double powThenAdd() {
return Math.pow(x1, 2) + Math.pow(x1, 2);
}
@Benchmark
public void useBlackhole(Blackhole hole) {
//将结果存放到 black hole,因此两次pow操作都会生效
hole.consume(Math.pow(x1, 2));
hole.consume(Math.pow(x2, 2));
}
public static void main(String[] args) throws RunnerException {
final Options opts = new OptionsBuilder().include(JMHExample014.class.getSimpleName())
.build();
new Runner(opts).run();
}
}
3 避免常量折叠(Constant Folding)
常量折叠是Java编译器早期的一种优化——编译优化。在javac对源文件进行编译的过程中,通过语法分析可以发现某些常量是可以被折叠的,也就是可以直接将计算结果存放到声明中,而不需要在执行阶段再次进行运算。例如
private final int x = 10;
private final int y = x*20;
在编译阶段,y的值被直接赋予200,这就是所谓的常量折叠。
4 避免循环展开(Loop Unwinding)
我们在编写JMH代码的时候,除了要避免Dead Code以及减少常量的引用之外,还要尽可能地避免或者减少基准测试代码方法中出现循环,因为循环代码在运行阶段(JVM后期优化)极有可能被“痛下杀手”进行相关优化,这种优化被称为循环展开。如下
int sum=0;
for (int i = 0; i < 100; i++) {
sum+=i;
}
其中sum=sum+i
这样的代码会被执行100次,也就是说,JVM会向CPU发送100次这样的计算指令,。JVM的设计者会认为这样做的方式可以被优化为如下;
for (int i = 0; i < 20; i+=5) {
sum+=i;
sum+=i+1;
sum+=i+2;
sum+=i+3;
sum+=i+4;
}
优化后将循环体中的计算指令批量发送给CPU,这种批量的方式可以提高计算的效率。
import org.openjdk.jmh.annotations.*;
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.OptionsBuilder;
import java.util.concurrent.TimeUnit;
/**
* @author MI
* @version 1.0
* @date 2021/5/15 19:46
*/
@BenchmarkMode(Mode.AverageTime)
@Fork(1)
@Warmup(iterations = 5)
@Measurement(iterations = 10)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
//设置为线程间共享的资源
@State(Scope.Thread)
public class JMHExample016 {
private int x = 1;
private int y = 2;
@Benchmark
public int measure() {
return (x + y);
}
private int loopCpmpute(int times) {
int result = 0;
for (int i = 0; i < times; i++) {
result += (x + y);
}
return result;
}
@OperationsPerInvocation
@Benchmark
public int measureLoop_1() {
return loopCpmpute(1);
}
@OperationsPerInvocation
@Benchmark
public int measureLoop_10() {
return loopCpmpute(10);
}
@OperationsPerInvocation
@Benchmark
public int measureLoop_100() {
return loopCpmpute(100);
}
@OperationsPerInvocation
@Benchmark
public int measureLoop_1000() {
return loopCpmpute(1000);
}
public static void main(String[] args) throws RunnerException {
final Options opts = new OptionsBuilder().include(JMHExample016.class.getSimpleName())
.build();
new Runner(opts).run();
}
}
5 Fork用于避免Profile-guided optimizations
import org.openjdk.jmh.annotations.*;
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.OptionsBuilder;
import java.util.concurrent.TimeUnit;
/**
* @author MI
* @version 1.0
* @date 2021/5/15 19:46
*/
@BenchmarkMode(Mode.AverageTime)
@Fork(0)
@Warmup(iterations = 5)
@Measurement(iterations = 5)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
//设置为线程间共享的资源
@State(Scope.Thread)
public class JMHExample017 {
//Inc1 Inc2 完全一样
interface Inc {
int inc();
}
public static class Inc1 implements Inc {
private int i = 0;
@Override
public int inc() {
return ++i;
}
}
public static class Inc2 implements Inc {
private int i = 0;
@Override
public int inc() {
return ++i;
}
}
private Inc inc1 = new Inc1();
private Inc inc2 = new Inc2();
private int measure(Inc inc) {
int result = 0;
for (int i = 0; i < 10; i++) {
result += inc.inc();
}
return result;
}
@Benchmark
public int measure_inc_1() {
return this.measure(inc1);
}
@Benchmark
public int measure_inc_2() {
return this.measure(inc2);
}
@Benchmark
public int measure_inc_3() {
return this.measure(inc1);
}
public static void main(String[] args) throws RunnerException {
final Options opts = new OptionsBuilder().include(JMHExample017.class.getSimpleName())
.build();
new Runner(opts).run();
}
}
将Fork设置为0,每一个基准测试方法都将会与JMHExample017使用同一个JVM进程,因此基准测试方法可能混入JM进程的Profiler,结果如下,虽然measure_inc_1和measure_inc_2实现方式几乎一直,他们的性能却存在着较大的差距。虽然measure_inc_1和measure_inc_3代码实现完全相同,但还存在着不同的性能数据,这其实是JVM Profiler-guided optimizations导致的,由于我们所有的基准测试方法都与JMHExample017的JVM进程共享,因此难免在其中混入JMHExample017进程的Profiler。但是将Fork设置为1的时候,也就说每一次运行基准测试时都会开辟一个全新的JVM进程对其进行测试,那么多个基准测试之间就不会存在干扰了。
Benchmark Mode Cnt Score Error Units
JMHExample017.measure_inc_1 avgt 5 0.002 ± 0.001 us/op
JMHExample017.measure_inc_2 avgt 5 0.005 ± 0.001 us/op
JMHExample017.measure_inc_3 avgt 5 0.005 ± 0.001 us/op
将Fork设置为1
Benchmark Mode Cnt Score Error Units
JMHExample017.measure_inc_1 avgt 5 0.004 ± 0.001 us/op
JMHExample017.measure_inc_2 avgt 5 0.004 ± 0.001 us/op
JMHExample017.measure_inc_3 avgt 5 0.004 ± 0.001 us/op
1.4.2 一些高级的用法
1 Asymmetric Benchmark
JMH会根据方法名的字典顺序排序之后串行执行,然而有时候我们会想对某个类的读写方法并行执行,比如,想要在修改某个原子变量的时候又有其他线程对其进行读操作。
import org.openjdk.jmh.annotations.*;
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.OptionsBuilder;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author MI
* @version 1.0
* @date 2021/5/15 19:46
*/
@BenchmarkMode(Mode.AverageTime)
@Fork(1)
@Warmup(iterations = 5)
@Measurement(iterations = 5)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
//设置为线程间共享的资源
@State(Scope.Group)
public class JMHExample018 {
private AtomicInteger counter;
@Setup
public void init() {
this.counter = new AtomicInteger();
}
@GroupThreads(5)
@Group("q")
@Benchmark
public void inc() {
this.counter.incrementAndGet();
}
@GroupThreads(5)
@Group("q")
@Benchmark
public void get() {
this.counter.get();
}
public static void main(String[] args) throws RunnerException {
final Options opts = new OptionsBuilder().include(JMHExample018.class.getSimpleName())
.build();
new Runner(opts).run();
}
}
2 interrupts Benchmark
测试执行某些容器的读写操作是可能引起的阻塞,但这种阻塞并不是容器无法保证线程安全问题引起的,而是由JMH框架的机制引起的。
eclipse等开发工具配置
java微基准测试JMH引入报错RuntimeException: ERROR: Unable to find the resource: /META-INF/BenchmarkList
eclipse需要安装apt插件,从eclipse marketplace上搜索apt,找到m2e-apt x.x.x 安装它。
然后maven启用apt