Java使用JMH进行简单的基准测试Benchmark

这里说道的基准测试Benchmark其实是微基准测试Micro-Benchmark。这里面的概念就不详细介绍了,反正就是JMH可以非常方便的帮助我们进行java代码的简单基准测试。

有什么用

可以对多组代码进行基准测试比较。

很多人总说,这样用速度快,性能好,别听他们的,自己试过才知道。

Java的基准测试需要注意的几个点

测试前需要预热。
防止无用代码进入测试方法中。
并发测试。
测试结果呈现。

简单的代码例子

我们来看一个简单的示例,大家就知道JMH的强大了。

测试代码如下。

package me.irfen.jmh;

import org.openjdk.jmh.annotations.*;

import java.util.concurrent.TimeUnit;

/**
 * author: zhaoye
 * date: 2016/11/4 14:36
 */
@BenchmarkMode(Mode.Throughput)
@Warmup(iterations = 3)
@Measurement(iterations = 10, time = 5, timeUnit = TimeUnit.SECONDS)
@Threads(16)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class StringBuilderTest {

    @Benchmark
    public void testStringAdd() {
        String a = "";
        for (int i = 0; i < 10; i++) {
            a += i;
        }
        print(a);
    }

    @Benchmark
    public void testStringBuilderAdd() {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 10; i++) {
            sb.append(i);
        }
        print(sb.toString());
    }

    private void print(String a) {
    }
}

代码中有好多注解,我们一会再说,先说我们要干什么。这里应该很好看出来,大家都说String进行字符相加的时候没有StringBuilder快,怎么证明,则就是我们要做的事情。

现在来解释注解

@Setup

这次我们没用到,简单说,这个注解的作用就是我们需要在测试之前进行一些准备工作,比如对一些数据的初始化之类的。。

@BenchmarkMode

基准测试类型。这里选择的是Throughput也就是吞吐量。根据源码点进去,每种类型后面都有对应的解释,比较好理解,吞吐量会得到单位时间内可以进行的操作数。

下面看下这里的源码。

Throughput("thrpt", "Throughput, ops/time"),
AverageTime("avgt", "Average time, time/op"),
SampleTime("sample", "Sampling time"),
SingleShotTime("ss", "Single shot invocation time"),
All("all", "All benchmark modes");

@Warmup

上面我们提到了,进行基准测试前需要进行预热。一般我们前几次进行程序测试的时候都会比较慢,所以要让程序进行几轮预热,保证测试的准确性。其中的参数iterations也就非常好理解了,就是预热轮数。

@Measurement

度量,其实就是一些基本的测试参数。iterations进行测试的轮次,time每轮进行的时长,timeUnit时长单位。都是一些基本的参数,可以根据具体情况调整。一般比较重的东西可以进行大量的测试,放到服务器上运行。

@Threads

测试线程,这个非常好理解,根据具体情况选择,一般为cpu乘以2。

@OutputTimeUnit

这个比较简单了,基准测试结果的时间类型。一般选择秒或者毫秒。

@State

当使用@Setup参数的时候,必须在类上加这个参数,不然会提示无法运行。

执行方式

生成jar执行

一般对于大型的测试,需要测试时间比较久,线程比较多的话,就需要去写好了丢到linux程序里执行,不然本机执行很久时间什么都干不了了。

$ mvn clean install
$ java -jar target/benchmarks.jar

首先编译,然后执行就可以了。当然在执行的时候可以输入-h参数来看帮助。

在IDE中执行

对于一些我们自己的一些小测试,就可以直接在IDE中执行了,还丢到linux上去太麻烦了。怎么搞呢,看下面。

package me.irfen.jmh;

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;

public class Test {

    public static void main(String[] args) throws RunnerException {
        Options options = new OptionsBuilder().include(StringBuilderTest.class.getSimpleName())
                .output("E:/Benchmark.log").forks(2).build();
        new Runner(options).run();
    }
}

执行的方法当然还是个main方法,当然你用junit也是可以的。这里其实也比较简单,new个Options,然后传入要运行哪个测试,选择基准测试报告输出文件地址,设置fork出的线程数,然后通过Runner的run方法就可以跑起来了。

报告结果

下面我们来看下报告结果。

# JMH 1.14.1 (released 45 days ago)
# VM version: JDK 1.8.0_71, VM 25.71-b15
# VM invoker: C:\Program Files\Java\jdk1.8.0_71\jre\bin\java.exe
# VM options: -Didea.launcher.port=7532 -Didea.launcher.bin.path=C:\Program Files (x86)\JetBrains\IntelliJ IDEA 2016.2.4\bin -Dfile.encoding=UTF-8
# Warmup: 3 iterations, 1 s each
# Measurement: 10 iterations, 5 s each
# Timeout: 10 min per iteration
# Threads: 16 threads, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: me.irfen.jmh.StringBuilderTest.testStringAdd

# Run progress: 0.00% complete, ETA 00:03:32
# Fork: 1 of 2
# Warmup Iteration   1: 14690.103 ops/ms
# Warmup Iteration   2: 18731.179 ops/ms
# Warmup Iteration   3: 19475.241 ops/ms
Iteration   1: 18787.224 ops/ms
Iteration   2: 18949.244 ops/ms
Iteration   3: 18715.979 ops/ms
Iteration   4: 18770.003 ops/ms
Iteration   5: 18912.926 ops/ms
Iteration   6: 19252.784 ops/ms
Iteration   7: 18975.545 ops/ms
Iteration   8: 18767.721 ops/ms
Iteration   9: 18976.551 ops/ms
Iteration  10: 18830.076 ops/ms

# Run progress: 25.00% complete, ETA 00:03:05
# Fork: 2 of 2
# Warmup Iteration   1: 15871.759 ops/ms
# Warmup Iteration   2: 19330.148 ops/ms
# Warmup Iteration   3: 18851.970 ops/ms
Iteration   1: 19058.934 ops/ms
Iteration   2: 18851.454 ops/ms
Iteration   3: 18511.584 ops/ms
Iteration   4: 18951.476 ops/ms
Iteration   5: 18721.372 ops/ms
Iteration   6: 18891.480 ops/ms
Iteration   7: 18651.455 ops/ms
Iteration   8: 18854.140 ops/ms
Iteration   9: 19198.246 ops/ms
Iteration  10: 19148.773 ops/ms


Result "testStringAdd":
  18888.848 ±(99.9%) 160.591 ops/ms [Average]
  (min, avg, max) = (18511.584, 18888.848, 19252.784), stdev = 184.936
  CI (99.9%): [18728.258, 19049.439] (assumes normal distribution)


# JMH 1.14.1 (released 45 days ago)
# VM version: JDK 1.8.0_71, VM 25.71-b15
# VM invoker: C:\Program Files\Java\jdk1.8.0_71\jre\bin\java.exe
# VM options: -Didea.launcher.port=7532 -Didea.launcher.bin.path=C:\Program Files (x86)\JetBrains\IntelliJ IDEA 2016.2.4\bin -Dfile.encoding=UTF-8
# Warmup: 3 iterations, 1 s each
# Measurement: 10 iterations, 5 s each
# Timeout: 10 min per iteration
# Threads: 16 threads, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: me.irfen.jmh.StringBuilderTest.testStringBuilderAdd

# Run progress: 50.00% complete, ETA 00:02:04
# Fork: 1 of 2
# Warmup Iteration   1: 84345.885 ops/ms
# Warmup Iteration   2: 118411.934 ops/ms
# Warmup Iteration   3: 69339.009 ops/ms
Iteration   1: 74770.077 ops/ms
Iteration   2: 76996.804 ops/ms
Iteration   3: 77855.877 ops/ms
Iteration   4: 76883.502 ops/ms
Iteration   5: 75685.537 ops/ms
Iteration   6: 76299.372 ops/ms
Iteration   7: 77065.766 ops/ms
Iteration   8: 76537.937 ops/ms
Iteration   9: 77216.754 ops/ms
Iteration  10: 75410.698 ops/ms

# Run progress: 75.00% complete, ETA 00:01:02
# Fork: 2 of 2
# Warmup Iteration   1: 78998.331 ops/ms
# Warmup Iteration   2: 61750.531 ops/ms
# Warmup Iteration   3: 69513.539 ops/ms
Iteration   1: 64862.481 ops/ms
Iteration   2: 65963.130 ops/ms
Iteration   3: 66719.366 ops/ms
Iteration   4: 66315.918 ops/ms
Iteration   5: 67172.960 ops/ms
Iteration   6: 66156.558 ops/ms
Iteration   7: 67372.587 ops/ms
Iteration   8: 67607.029 ops/ms
Iteration   9: 67339.769 ops/ms
Iteration  10: 65845.035 ops/ms


Result "testStringBuilderAdd":
  71503.858 ±(99.9%) 4491.696 ops/ms [Average]
  (min, avg, max) = (64862.481, 71503.858, 77855.877), stdev = 5172.644
  CI (99.9%): [67012.162, 75995.554] (assumes normal distribution)


# Run complete. Total time: 00:04:11

Benchmark                                Mode  Cnt      Score      Error   Units
StringBuilderTest.testStringAdd         thrpt   20  18888.848 ±  160.591  ops/ms
StringBuilderTest.testStringBuilderAdd  thrpt   20  71503.858 ± 4491.696  ops/ms

内容有点多,仔细看,三大部分,第一部分是有序的结果,第二部分是无序的结果,第三部分就是两个的简单结果比较。这里注意我们forks传的2,所以每个测试有两个fork结果。

前两部分是一样的,简单说下。首先会写出每部分的一些参数设置,然后是预热迭代执行(Warmup Iteration),然后是正常的迭代执行(Iteration),最后是结果(Result)。这些看看就好,我们最关注的就是第三部分,其实也就是最终的结论。千万别看歪了,他输出的也确实很不爽,error那列其实没有内容,score的结果是xxx ± xxx,单位是没毫秒多少个操作。可以看到,StringBuilder的速度还确实是要比String进行文字叠加的效率好太多。

最后

到此我们介绍了JMH的一些简单用法。对于别人说的东西,现在jvm优化了很多,哪些还是真的,哪些是已经过时的?只有自己试过了才知道。

官网在这,http://openjdk.java.net/projects/code-tools/jmh/
其他还有很多有趣的例子,http://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/

本文原创于赵伊凡BLOG转载请注明出处。

©原创文章,转载请注明来源: 赵伊凡's Blog
©本文链接地址: Java使用JMH进行简单的基准测试Benchmark