[性能优化一]不服跑个分-Java微基准测试框架JMH

1. 为什么需要JMH

某些场景下需要精确地知道一段代码的性能如何,如:

  1. 当你已经找出了热点函数,需要对热点函数进行进一步优化时;
  2. 想定量地知道某个函数需要执行多长时间,以及执行时间和输入 n 的相关性;
  3. 一个函数有两种不同实现(例如JSON序列化/反序列化有Jackson和Gson实现),不知道哪种实现性能更好。

最简单的做法是在代码执行前后记录下时间,然后计算一下时间差,如:

long start = System.currentTimeMillis();
// 待测试的代码块...
System.out.println(System.currentTimeMillis() - start);

但是这样做会有如下几个问题:

  1. System.currentTimeMillis() 如函数自身注释所说,本身精度有限,根据操作系统不同,会存在数十毫秒左右的的误差;
  2. JVM 在运行时会进行代码预热,说白了就是越跑越快,因为类需要装载、需要准备操作;
  3. JVM 会在各个阶段都有可能对代码进行优化处理,比如某个计算的结果没有被使用,那么这段代码在执行时就会被忽略,这样的问题比较难察觉;
  4. JVM垃圾回收的不确定性,可能运行很快,回收很慢;
  5. 使用不方便,配置不灵活,如果需要打印多种类型的测试数据,就需要增加很多额外的代码,
    不容易修改测试的类型和条件。

因此,使用一款靠谱的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

参考

  1. https://github.com/openjdk/jmh
  2. Java微基准测试框架JMH
  3. Java benchmark 工具 JMH
  4. JMH - Java 代码性能基准测试
  5. https://stackoverflow.com/questions/16228992/commons-lang-stringutils-replace-performance-vs-string-replace
  6. https://github.com/greenlaw110/java-str-benchmark

你可能感兴趣的:([性能优化一]不服跑个分-Java微基准测试框架JMH)