基准测试 (Benchmark) 入门篇

基准测试对我们来说,一个熟悉又陌生的名字。说它熟悉的原因是它在我们生活中无处不在;说他陌生,是因为它常常以各种名字存在于我们生活中。比如”不服跑个分“,其中的“跑分”指的就是基准测试。类似的还有网速测试、Online Judge 的评测结果等等。本文将介绍部分基准测试的理论、以及 Java 官方提供的微基准测试工具的使用。

什么是基准测试

那么什么是基准测试呢?

简单来说,基准测试是为了评估测试目标的性能采取的一系列行动,包括运行一个或一组程序或者执行其他的操作。[1]

基准测试按照其测试用例是否接近于真实运行的环境,分为微基准测试(Micro-Benchmarking)、模拟、回放、工业标准(生产环境)测试。[2]

微基准测试 使用人造的测试用例对某类特定的操作做测试。这类特定的操作往往指的是软件的一小部分(比如一个方法)。

这篇文章主要介绍的是微基准测试。

基准测试应该注意的问题

基准测试不是随意的测试

基准测试不是仅仅执行一下,记一下结果就可以的。比如常常出现的问题就是忽略缓存对于测试结果的影响。

好的基准测试应该检查以下几点:

  1. 严格检查实际测试的是什么
  2. 确定理解测试了什么:对测试结果进行分析
  3. 得出有效的结论

控制变量的个数

为保证得出正确的结论,控制不同的测试中除了被测试的变量,确保没有其他变量对结果造成影响。这个往往是比较难以达到的,比如对 Java 程序进行微基准测试,需要注意编译器优化(包括编译期优化和运行时优化 JIT)、机器负载、操作系统的缓存等等变量。做到这些需要使用合适的工具加上测试者对这些有一定的了解。

基准测试的环境和配置

基准测试之前,需尽力保证所测系统运行在最佳的系统和配置中,系统的性能以及达到真正的极限。比如”不服跑分“的测试中,确保被测手机的电量充足、系统没有开启省电模式等。

Java 中的微基准测试 JMH

Java 官方提供了一个基准测试工具 JMH(Java Microbenchmark Harness),我们可以用它测试

  1. 一段代码平均执行多长时间
  2. 对比不同实现的性能

第一个微基准测试工程

最为便捷的是使用 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() {
// This is a demo/sample template for building your JMH benchmarks. Edit as needed.
// Put your benchmark code here.
}
}

其中,我们可以在 testMethod 填写基准测试代码。

这里以声明一个字符串举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import org.openjdk.jmh.annotations.Benchmark;

/***
*
* Benchmark Mode Cnt Score Error Units
* MyBenchmark.testMethod thrpt 25 217365928.196 ± 6111510.557 ops/s
*
*/

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 的使用方式,以及提供几个基准测试案例,来验证几个提高性能的方法。

参考资料


  1. 维基百科中对于基准测试的定义 https://en.wikipedia.org/wiki/Benchmark_(computing) ↩︎

  2. Gregg, Brendan. Systems performance: enterprise and the cloud. Pearson Education, 2013. 中文译为 ”性能之巅“ ↩︎