1.2 JMH快速入门
在开始学习JMH之前,我们先来看一个不太严谨的性能测试方案(也许你平时就是这么做的),比较常见的List有ArrayList和LinkedList,在选取这两个API作为你程序中的数据容器时,我们先对比一下这两者的add方法在性能上的差异,从而挑选出一个对性能较好的容器应用在程序开发之中。
1.2.1 用main方法进行测试
程序员通常会将一些简单的测试交给一个main函数来完成,比如对比两种List容器的add方法的性能时,我们将分别对其进行十组测试,每组测试都将会对List执行1 000 000次的add调用,然后通过最大值、最小值、平均值的方式对其进行对比(当然,这里并不考虑不同容器的内存开销,事实上,ArrayList和LinkedList在使用的过程中,它们的内存开销肯定是不一样的)。
程序代码:ArrayListVSLinkedList.java
package com.wangwenjun.concurrent.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; public class ArrayListVSLinkedList { private final static String DATA = "DUMMY DATA"; private final static int MAX_CAPACITY = 1_000_000; private final static int MAX_ITERATIONS = 10; private static void test(List<String> list) { for (int i = 0; i < MAX_CAPACITY; i++) { list.add(DATA); } } private static void arrayListPerfTest(int iterations) { for (int i = 0; i < iterations; i++) { final List<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 List<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("#", 100)); linkedListPerfTest(MAX_ITERATIONS); } }
运行上面的程序,我们会得出如下的数据(如图1-1所示)。
图1-1 ArrayList vs LinkedList add方法性能
▪ ArrayList:最大耗时304毫秒,最小耗时18毫秒,平均耗时83.2毫秒。
▪ LinkedList:最大耗时455毫秒,最小耗时21毫秒,平均耗时104毫秒。
乍一看ArrayList的add方法性能要好于LinkedList的add方法,事实上,ArrayList的随机读写性能确实要好于LinkedList(尤其是在ArrayList不进行内部扩容数组复制的情况下),LinkedList由于链表的设计,其delete操作的性能会好于ArrayList,无论怎样,我们的这种测试仍旧存在诸多问题,具体列举如下。
▪ 使用Stopwatch进行时间计算,其实是在Stopwatch内部记录了方法执行的开始纳秒数,这种操作本身会导致一些CPU时间的浪费。
▪ 在代码的运行过程中,JVM可能会对其进行运行时的优化,比如循环展开、运行时编译等,这样会导致某组未经优化的性能数据参与统计计算。
▪ arrayListPerfTest方法和linkedListPerfTest的运行环境并不公平,比如,在第一个测试方法执行的过程中或者执行结束后,其所在的JVM进程或许已经进行了profiler的优化,还有第一个测试方法所开辟的内存有可能也未被释放。
1.2.2 用JMH进行微基准测试
经过1.2.1节中的讨论,我们大体上知道要做一个严谨的基准测试并不是一件容易的事情,幸运的是,JMH的开发者已经为我们提供了对应的解决方案,本节就来看一下如何使用JMH对LinkedList和ArrayList的add方法进行性能测试。
首先,要将JMH的依赖加入我们的工程之中,笔者使用的是JMH的1.19版本,截至本书写作的时候,JMH最新最稳定的版本为1.19版。
<dependency> <groupId>org.openjdk.jmh</groupId> <artifactId>jmh-core</artifactId> <version>1.19</version> </dependency> <dependency> <groupId>org.openjdk.jmh</groupId> <artifactId>jmh-generator-annprocess</artifactId> <version>1.19</version> <scope>provided</scope> </dependency>
程序代码:JMHExample01.java
package com.wangwenjun.concurrent.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.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.concurrent.TimeUnit; @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 this.linkedList; } public static void main(String[] args) throws RunnerException { final Options opts = new OptionsBuilder().include(JMHExample01.class.getSimpleName()) .forks(1) .measurementIterations(10) .warmupIterations(10) .build(); new Runner(opts).run(); } }
上面的程序中,我们使用了大量的JMH API,也许暂时你还不太熟悉甚至还不知道它们的用法,不过不必担心,后文中会逐一进行讲解。首先运行上面的程序,会得到非常多的信息输出,程序输出如下所示。
程序输出:JMHExample01.java
# JMH version: 1.19 # VM version: JDK 1.8.0_40-ea, VM 25.40-b11 # VM invoker: D:\Program Files\Java\jdk1.8.0_40\jre\bin\java.exe # VM options: -javaagent:C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2017.2\lib\idea_rt.jar=52719:C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2017.2\bin -Dfile.encoding=UTF-8 # Warmup: 10 iterations, 1 s each # Measurement: 10 iterations, 1 s each # Timeout: 10 min per iteration # Threads: 1 thread, will synchronize iterations # Benchmark mode: Average time, time/op # Benchmark: com.wangwenjun.concurrent.jmh.JMHExample01.arrayListAdd # Run progress: 0.00% complete, ETA 00:00:40 # Fork: 1 of 1 # Warmup Iteration 1: 0.074 us/op # Warmup Iteration 2: 0.412 us/op # Warmup Iteration 3: 0.163 us/op # Warmup Iteration 4: 0.492 us/op # Warmup Iteration 5: 0.058 us/op # Warmup Iteration 6: 0.057 us/op # Warmup Iteration 7: 0.042 us/op # Warmup Iteration 8: 0.092 us/op # Warmup Iteration 9: 0.042 us/op # Warmup Iteration 10: 0.056 us/op Iteration 1: 0.043 us/op Iteration 2: 0.083 us/op Iteration 3: 0.046 us/op Iteration 4: 0.038 us/op Iteration 5: 0.034 us/op Iteration 6: 0.085 us/op Iteration 7: 0.037 us/op Iteration 8: 0.042 us/op Iteration 9: 0.042 us/op Iteration 10: 0.035 us/op Result "com.wangwenjun.concurrent.jmh.JMHExample01.arrayListAdd": 0.048 ±(99.9%) 0.029 us/op [Average] (min, avg, max) = (0.034, 0.048, 0.085), stdev = 0.019 CI (99.9%): [0.020, 0.077] (assumes normal distribution) # JMH version: 1.19 # VM version: JDK 1.8.0_40-ea, VM 25.40-b11 # VM invoker: D:\Program Files\Java\jdk1.8.0_40\jre\bin\java.exe # VM options: -javaagent:C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2017.2\lib\idea_rt.jar=52719:C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2017.2\bin -Dfile.encoding=UTF-8 # Warmup: 10 iterations, 1 s each # Measurement: 10 iterations, 1 s each # Timeout: 10 min per iteration # Threads: 1 thread, will synchronize iterations # Benchmark mode: Average time, time/op # Benchmark: com.wangwenjun.concurrent.jmh.JMHExample01.linkedListAdd # Run progress: 50.00% complete, ETA 00:00:27 # Fork: 1 of 1 # Warmup Iteration 1: 0.174 us/op # Warmup Iteration 2: 2.838 us/op # Warmup Iteration 3: 0.995 us/op # Warmup Iteration 4: 0.953 us/op # Warmup Iteration 5: 0.202 us/op # Warmup Iteration 6: 1.029 us/op # Warmup Iteration 7: 0.238 us/op # Warmup Iteration 8: 0.191 us/op # Warmup Iteration 9: 0.192 us/op # Warmup Iteration 10: 0.177 us/op Iteration 1: 1.040 us/op Iteration 2: 0.192 us/op Iteration 3: 1.067 us/op Iteration 4: 0.186 us/op Iteration 5: 1.107 us/op Iteration 6: 0.188 us/op Iteration 7: 1.023 us/op Iteration 8: 0.179 us/op Iteration 9: 1.041 us/op Iteration 10: 0.220 us/op Result "com.wangwenjun.concurrent.jmh.JMHExample01.linkedListAdd": 0.624 ±(99.9%) 0.688 us/op [Average] (min, avg, max) = (0.179, 0.624, 1.107), stdev = 0.455 CI (99.9%): [ ≈ 0, 1.313] (assumes normal distribution) # Run complete. Total time: 00:02:19 Benchmark Mode Cnt Score Error Units JMHExample01.arrayListAdd avgt 10 0.048 ± 0.029 us/op JMHExample01.linkedListAdd avgt 10 0.624 ± 0.688 us/op
虽然输出的信息很多,但是目前我们只需要查看输出的最后两行,大体上,我们从这两行信息可以发现arrayListAdd方法的调用平均响应时间为0.048微秒,误差在0.029微秒,而linkedListAdd方法的调用平均响应时间为0.624微秒,误差在0.688微秒,很明显,前者的性能是要高于后者的,虽然从结果上来看,这与我们之前利用main函数的测试方法得出的结论是一致的,但是显然JMH要严谨科学很多。