基准测试对我们来说,一个熟悉又陌生的名字。说它熟悉的原因是它在我们生活中无处不在;说他陌生,是因为它常常以各种名字存在于我们生活中。比如”不服跑个分“,其中的“跑分”指的就是基准测试。类似的还有网速测试、Online Judge 的评测结果等等。本文将介绍部分基准测试的理论、以及 Java 官方提供的微基准测试工具的使用。
什么是基准测试那么什么是基准测试呢?
简单来说,基准测试 是为了评估测试目标的性能采取的一系列行动,包括运行一个或一组程序或者执行其他的操作。
基准测试按照其测试用例是否接近于真实运行的环境,分为微基准测试(Micro-Benchmarking)、模拟、回放、工业标准(生产环境)测试。
微基准测试 使用人造的测试用例对某类特定的操作做测试。这类特定的操作往往指的是软件的一小部分(比如一个方法)。
这篇文章主要介绍的是微基准测试。
基准测试应该注意的问题 基准测试不是随意的测试基准测试不是仅仅执行一下,记一下结果就可以的。比如常常出现的问题就是忽略缓存对于测试结果的影响。
好的基准测试应该检查以下几点:
严格检查实际测试的是什么 确定理解测试了什么:对测试结果进行分析 得出有效的结论 控制变量的个数为保证得出正确的结论,控制不同的测试中除了被测试的变量,确保没有其他变量对结果造成影响。这个往往是比较难以达到的,比如对 Java 程序进行微基准测试,需要注意编译器优化(包括编译期优化和运行时优化 JIT)、机器负载、操作系统的缓存等等变量。做到这些需要使用合适的工具加上测试者对这些有一定的了解。
基准测试的环境和配置基准测试之前,需尽力保证所测系统运行在最佳的系统和配置中,系统的性能以及达到真正的极限。比如”不服跑分“的测试中,确保被测手机的电量充足、系统没有开启省电模式等。
Java 中的微基准测试 JMHJava 官方提供了一个基准测试工具 JMH (Java Microbenchmark Harness),我们可以用它测试
一段代码平均执行多长时间 对比不同实现的性能 第一个微基准测试工程最为便捷的是使用 Maven 创建项目:
1 2 3 4 5 6 7 mvn archetype:generate \ -DinteractiveMode=false \ -DarchetypeGroupId=org.openjdk.jmh \ -DarchetypeArtifactId=jmh-java-benchmark-archetype \ -DgroupId=com.technologiesinsight.jmh \ -DartifactId=jmh-demo \ -Dversion=1.0
构建完成后,会发现一个文件夹 jmh-demo。这是一个标准的 maven 工程,会有一个默认的测试类:MyBenchmark,位于 com.technologiesinsight.jmh 包下,也就是上面命令的 -DgroupId
填写的包名。测试类内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 package com.technologiesinsight.jmh;import org.openjdk.jmh.annotations.Benchmark;public class MyBenchmark { @Benchmark public void testMethod () { } }
其中,我们可以在 testMethod 填写基准测试代码。
这里以声明一个字符串举例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import org.openjdk.jmh.annotations.Benchmark;public class MyBenchmark { @Benchmark public String testMethod () { return "hello world" ; } }
接下来就可以运行了,进入项目目录,执行 mvn clean package
打包,执行 java -jar target/benchmarks.jar
运行,控制台会出现类似如下信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 // 第一部分开始 # JMH version: 1.21 # VM version: JDK 1.8.0_201, Java HotSpot(TM) 64-Bit Server VM, 25.201-b09 # VM invoker: /Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/bin/java # VM options: -Dfile.encoding=UTF-8 # Warmup: 5 iterations, 10 s each # Measurement: 5 iterations, 10 s each # Timeout: 10 min per iteration # Threads: 1 thread, will synchronize iterations # Benchmark mode: Throughput, ops/time # Benchmark: com.technologiesinsight.jmh.MyBenchmark.testMethod // 第二部分开始 # Run progress: 0.00% complete, ETA 00:08:20 # Fork: 1 of 5 # Warmup Iteration 1: 188758589.759 ops/s # Warmup Iteration 2: 187065201.979 ops/s # Warmup Iteration 3: 206670810.488 ops/s # Warmup Iteration 4: 211958028.465 ops/s # Warmup Iteration 5: 224040261.103 ops/s Iteration 1: 224379265.753 ops/s Iteration 2: 208679880.245 ops/s Iteration 3: 206755976.374 ops/s Iteration 4: 214591855.201 ops/s Iteration 5: 219524929.562 ops/s // ……此处省略迭代信息 // 第三部分开始 Result "com.technologiesinsight.jmh.MyBenchmark.testMethod": 217365928.196 ±(99.9%) 6111510.557 ops/s [Average] (min, avg, max) = (204122743.394, 217365928.196, 233217556.736), stdev = 8158691.742 CI (99.9%): [211254417.640, 223477438.753] (assumes normal distribution) # Run complete. Total time: 00:08:22 REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial experiments, perform baseline and negative tests that provide experimental control, make sure the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts. Do not assume the numbers tell you what you want them to tell. Benchmark Mode Cnt Score Error Units MyBenchmark.testMethod thrpt 25 217365928.196 ± 6111510.557 ops/s
输出信息可以分为三部分,使用 // 标注。第一部分是执行的环境、参数信息。第二分每次迭代的详情,第三部分是基准测试的结果。
我们着重看最后报告部分:
1 2 Benchmark Mode Cnt Score Error Units MyBenchmark.testMethod thrpt 25 217365928.196 ± 6111510.557 ops/s
最重要的就是 Score 这一列,代表得分,单位是 ops/s 代表每秒钟可以执行多少次操作(其倒数就是平均运行一次测试用例需要多少时间)。因为赋值操作是十分轻量的操作,所以执行速度非常快,约为4.6纳秒就可以完成(未考虑测试框架本身微小的性能损耗)。
关于JMH 的运行方式运行方式有多种,除了上面提到的使用命令行 java -jar 运行,也可以使用 IDEA 插件和 mian 方法运行。
使用 IntelliJ IDEA 插件 :不需要写main 方法,也不需要打包,直接右键就可以运行。 使用 main 方法:不需要插件但需要写额外的 main 方法。 本系列的例子中提供了 helper 类方便的执行。 下一篇文章将会详细介绍 JMH 的使用方式,以及提供几个基准测试案例 ,来验证几个提高性能的方法。
参考资料