1. 为什么需要JMH
某些场景下需要精确地知道一段代码的性能如何,如:
- 当你已经找出了热点函数,需要对热点函数进行进一步优化时;
- 想定量地知道某个函数需要执行多长时间,以及执行时间和输入 n 的相关性;
- 一个函数有两种不同实现(例如JSON序列化/反序列化有Jackson和Gson实现),不知道哪种实现性能更好。
最简单的做法是在代码执行前后记录下时间,然后计算一下时间差,如:
long start = System.currentTimeMillis();
// 待测试的代码块...
System.out.println(System.currentTimeMillis() - start);
但是这样做会有如下几个问题:
- System.currentTimeMillis() 如函数自身注释所说,本身精度有限,根据操作系统不同,会存在数十毫秒左右的的误差;
- JVM 在运行时会进行代码预热,说白了就是越跑越快,因为类需要装载、需要准备操作;
- JVM 会在各个阶段都有可能对代码进行优化处理,比如某个计算的结果没有被使用,那么这段代码在执行时就会被忽略,这样的问题比较难察觉;
- JVM垃圾回收的不确定性,可能运行很快,回收很慢;
- 使用不方便,配置不灵活,如果需要打印多种类型的测试数据,就需要增加很多额外的代码,
不容易修改测试的类型和条件。
因此,使用一款靠谱的benchmark工具,既可以减少工作量,又可以确保性能优化过程不被错误的测试数据误导。
Java Microbenchmark Harness(JMH) 是由 Java 虚拟机团队开发的一款用于 Java的微基准测试工具,微基准是指方法(method)层面的测试基准,精度可以精确到微秒级。使用JMH 可以让你方便快速地进行一次严格的代码基准测试,并且有多种测试模式,多种测试维度可供选择。
2. JMH使用
JMH的使用可以参考官方示例 Code Sample ,本文则会介绍 JMH 最典型的用法和部分常用选项。
2.1 引入JMH依赖
在maven的配置文件中增加如下依赖,最新的依赖版本可以参考:
https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-core
https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-generator-annprocess
org.openjdk.jmh
jmh-core
1.35
org.openjdk.jmh
jmh-generator-annprocess
1.35
2.2 代码示例
这里以Java String替换为例进行说明,问题来源于Commons Lang StringUtils.replace performance vs String.replace,示例代码来自于java-str-benchmark,本文稍加改动,最终代码如附录1所示。
对比的替换方式有:
- String - JDK原生的字符串替换;
- StringUtils - Commons Lang的StringUtils.replace;
- Lang3StringUtils - Commons Lang 3的StringUtils.replace;
- OSGL - OSGL Java tool (2.0.0-SNAPSHOT);
- Fast - 自定义的快速字符串替换函数。
测试场景有:
- Short text - 将
AAAAAAAAAABBB
中的AA
替换成B
; - Short text no match -将
AAAAAAAAAABBB
中的XYZ
替换成B
; - Long text - 将长文本中的
occurrence
替换成appearance
; - Long text no match - 将长文本中的
aaaxyz0001
替换成appearance
。
2. 3. 执行结果
measure item | String | StringUtils | Lang3StringUtils | OSGL | Fast |
---|---|---|---|---|---|
short text | 0.316 us/op | 0.107 us/op | 0.102 us/op | 0.164 us/op | 0.105 us/op |
long text | 14.492 us/op | 6.860 us/op | 11.324 us/op | 9.887 us/op | 7.005 us/op |
short text no match | 0.121 us/op | 0.010 us/op | 0.010 us/op | 0.008 us/op | 0.009 us/op |
long text no match | 3.008 us/op | 2.298 us/op | 2.319 us/op | 1.302 us/op | 3.359 us/op |
附录
附录1 StringReplaceBenchmark.java
package com.liuil.core.benchmark;
import org.apache.commons.lang.StringUtils;
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 org.osgl.util.IO;
import org.osgl.util.S;
import java.util.concurrent.TimeUnit;
/**
* ref:
* https://stackoverflow.com/questions/16228992/commons-lang-stringutils-replace-performance-vs-string-replace
* https://github.com/greenlaw110/java-str-benchmark
*/
@BenchmarkMode(Mode.AverageTime)
@State(Scope.Thread)
@Fork(1)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Warmup(iterations = 5)
@Measurement(iterations = 5)
public class StringReplaceBenchmark {
public static final String TGT_NO_MATCH = "XYZ";
public static final String TGT = "AA";
public static final String REPLACEMENT = "B";
public static final String TEXT = "AAAAAAAAAABBB";
public static final String TGT_NO_MATCH_LONG = "aaaxyz0001";
public static final String TGT_LONG = "occurrence";
public static final String REP_LONG = "appearance";
public static final String TEXT_LONG = IO.readContentAsString(StringReplaceBenchmark.class.getResource("/long_str.txt"));
@State(Scope.Thread)
public static class BenchmarkState {
volatile private String str = TEXT;
volatile private String strLong = TEXT_LONG;
}
@Benchmark
public Object testString(BenchmarkState state) {
return state.str.replace(TGT, REPLACEMENT);
}
@Benchmark
public Object testStringUtils(BenchmarkState state) {
return StringUtils.replace(state.str, TGT, REPLACEMENT);
}
@Benchmark
public Object testLang3StringUtils(BenchmarkState state) {
return org.apache.commons.lang3.StringUtils.replace(state.str, TGT, REPLACEMENT);
}
@Benchmark
public Object testOsgl(BenchmarkState state) {
return S.have(state.str).replace(TGT).with(REPLACEMENT);
}
@Benchmark
public Object testFast(BenchmarkState state) {
return replace(state.str, TGT, REPLACEMENT);
}
@Benchmark
public Object testStringNoMatch(BenchmarkState state) {
return state.str.replace(TGT_NO_MATCH, REPLACEMENT);
}
@Benchmark
public Object testStringUtilsNoMatch(BenchmarkState state) {
return StringUtils.replace(state.str, TGT_NO_MATCH, REPLACEMENT);
}
@Benchmark
public Object testLang3StringUtilsNoMatch(BenchmarkState state) {
return org.apache.commons.lang3.StringUtils.replace(state.str, TGT_NO_MATCH, REPLACEMENT);
}
@Benchmark
public Object testOsglNoMatch(BenchmarkState state) {
return S.have(state.str).replace(TGT_NO_MATCH).with(REPLACEMENT);
}
@Benchmark
public Object testFastNoMatch(BenchmarkState state) {
return replace(state.str, TGT_NO_MATCH, REPLACEMENT);
}
@Benchmark
public Object testStringLong(BenchmarkState state) {
return state.strLong.replace(TGT_LONG, REP_LONG);
}
@Benchmark
public Object testStringUtilsLong(BenchmarkState state) {
return StringUtils.replace(state.strLong, TGT_LONG, REP_LONG);
}
@Benchmark
public Object testLang3StringUtilsLong(BenchmarkState state) {
return org.apache.commons.lang3.StringUtils.replace(state.strLong, TGT_LONG, REP_LONG);
}
@Benchmark
public Object testOsglLong(BenchmarkState state) {
return S.have(state.strLong).replace(TGT_LONG).with(REP_LONG);
}
@Benchmark
public Object testFastLong(BenchmarkState state) {
return replace(state.strLong, TGT_LONG, REP_LONG);
}
@Benchmark
public Object testStringLongNoMatch(BenchmarkState state) {
return state.strLong.replace(TGT_NO_MATCH_LONG, REPLACEMENT);
}
@Benchmark
public Object testStringUtilsLongNoMatch(BenchmarkState state) {
return StringUtils.replace(state.strLong, TGT_NO_MATCH_LONG, REPLACEMENT);
}
@Benchmark
public Object testLang3StringUtilsLongNoMatch(BenchmarkState state) {
return org.apache.commons.lang3.StringUtils.replace(state.strLong, TGT_NO_MATCH_LONG, REPLACEMENT);
}
@Benchmark
public Object testOsglLongNoMatch(BenchmarkState state) {
return S.have(state.strLong).replace(TGT_NO_MATCH_LONG).with(REPLACEMENT);
}
@Benchmark
public Object testFastLongNoMatch(BenchmarkState state) {
return replace(state.strLong, TGT_NO_MATCH_LONG, REPLACEMENT);
}
public static String replace(String source, String os, String ns) {
if (source == null) {
return null;
}
int i = 0;
if ((i = source.indexOf(os, i)) >= 0) {
char[] sourceArray = source.toCharArray();
char[] nsArray = ns.toCharArray();
int oLength = os.length();
StringBuilder buf = new StringBuilder(sourceArray.length);
buf.append(sourceArray, 0, i).append(nsArray);
i += oLength;
int j = i;
// Replace all remaining instances of oldString with newString.
while ((i = source.indexOf(os, i)) > 0) {
buf.append(sourceArray, j, i - j).append(nsArray);
i += oLength;
j = i;
}
buf.append(sourceArray, j, sourceArray.length - j);
source = buf.toString();
buf.setLength(0);
}
return source;
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder().include(StringReplaceBenchmark.class.getSimpleName()).build();
new Runner(opt).run();
}
}
附录2 执行结果
Benchmark Mode Cnt Score Error Units
StringReplaceBenchmark.testFast avgt 5 0.105 ± 0.012 us/op
StringReplaceBenchmark.testFastLong avgt 5 7.005 ± 0.170 us/op
StringReplaceBenchmark.testFastLongNoMatch avgt 5 3.359 ± 0.064 us/op
StringReplaceBenchmark.testFastNoMatch avgt 5 0.009 ± 0.001 us/op
StringReplaceBenchmark.testLang3StringUtils avgt 5 0.102 ± 0.002 us/op
StringReplaceBenchmark.testLang3StringUtilsLong avgt 5 11.324 ± 0.183 us/op
StringReplaceBenchmark.testLang3StringUtilsLongNoMatch avgt 5 2.319 ± 0.013 us/op
StringReplaceBenchmark.testLang3StringUtilsNoMatch avgt 5 0.010 ± 0.001 us/op
StringReplaceBenchmark.testOsgl avgt 5 0.164 ± 0.004 us/op
StringReplaceBenchmark.testOsglLong avgt 5 9.887 ± 0.340 us/op
StringReplaceBenchmark.testOsglLongNoMatch avgt 5 1.302 ± 0.031 us/op
StringReplaceBenchmark.testOsglNoMatch avgt 5 0.008 ± 0.002 us/op
StringReplaceBenchmark.testString avgt 5 0.316 ± 0.024 us/op
StringReplaceBenchmark.testStringLong avgt 5 14.492 ± 0.270 us/op
StringReplaceBenchmark.testStringLongNoMatch avgt 5 3.008 ± 0.050 us/op
StringReplaceBenchmark.testStringNoMatch avgt 5 0.121 ± 0.003 us/op
StringReplaceBenchmark.testStringUtils avgt 5 0.107 ± 0.002 us/op
StringReplaceBenchmark.testStringUtilsLong avgt 5 6.860 ± 0.290 us/op
StringReplaceBenchmark.testStringUtilsLongNoMatch avgt 5 2.298 ± 0.047 us/op
StringReplaceBenchmark.testStringUtilsNoMatch avgt 5 0.010 ± 0.001 us/op
参考
- https://github.com/openjdk/jmh
- Java微基准测试框架JMH
- Java benchmark 工具 JMH
- JMH - Java 代码性能基准测试
- https://stackoverflow.com/questions/16228992/commons-lang-stringutils-replace-performance-vs-string-replace
- https://github.com/greenlaw110/java-str-benchmark