使用JMH进行基准测试

什么是JMH

Java Microbenchmark Harness, 由JIT开发人员开发的,于2013年发布的一款基于Java的微基准测试工具,现已归于JDK。

官网http://openjdk.java.net/projects/code-tools/jmh/

JMH主要用于量化Java代码的性能,主要应用场景如下:

  • 对于已知的热点函数进行进一步优化

  • 确认函数执行时间以及于参数的关系

  • 比较相同功能的函数的性能

使用JMH

  1. 添加JMH依赖

 <!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-core -->
<dependency>
	<groupId>org.openjdk.jmh</groupId>
	<artifactId>jmh-core</artifactId>
	<version>1.21</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-generator-annprocess -->
<dependency>
	<groupId>org.openjdk.jmh</groupId>
	<artifactId>jmh-generator-annprocess</artifactId>
	<version>1.21</version>
	<scope>test</scope>
</dependency>
  1. 安装JMH插件,同Junit类似,测试方法的运行要借助于插件才可运行:分别在IDEA于Eclipse插件商店中搜索插件JMH

  2. 开启IDEA注解处理: compiler -> Annotation Processors -> Enable Annotation Processing

  3. 定义测试方法,打上注解 @Benchmark标记方法为测试方法,并使用IDEA运行

  4. 运行结果即为测试报告


注意:

在运行时可能发生如下异常:

ERROR: org.openjdk.jmh.runner.RunnerException: ERROR: Exception while trying to acquire the JMH lock (C:\WINDOWS\/jmh.lock): 拒绝访问。, exiting. Use -Djmh.ignoreLock=true to forcefully continue.
        at org.openjdk.jmh.runner.Runner.run(Runner.java:216)
        at org.openjdk.jmh.Main.main(Main.java:71)

此异常是因为JMH运行需要访问系统的TMP目录,TMP目录定义在环境变量上,然后在此路径下创建lock文件,由于我们未定义TMP环境变量,它默认会去C:\WINDOWS\ 路径下创建,此目录要求管理员权限,所以需要修改lock文件的路径;操作系统默认定义了一个临时目录的系统环境变量,我们也可以将系统环境变量加入到我们的启动配置中:

RunConfiguration -> Environment Variables -> include system environment viables

关于错误Unable to find the resource: /META-INF/BenchmarkList ...,出现这种错误有可能有如下几种情况:

  • 只添加了org.openjdk.jmh:jmh-core,缺少org.openjdk.jmh:jmh-generator-annprocess依赖

  • 测试类没有放在src/test路径下,而是放在了src/main

范例

package com.yangsx95.test;

import org.openjdk.jmh.annotations.Benchmark;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

/**
* @author yangsx
* @version 1.0
* @date 2019/10/20
*/
public class Test {
	static List<Integer> nums = new ArrayList<>();

	static {
		Random r = new Random();
		for (int i = 0; i < 10000; i++) nums.add(1000000 + r.nextInt(1000000));
	}

	static void foreach() {
		nums.forEach(v -> isPrime(v));
	}

	static void parallel() {
		nums.parallelStream().forEach(Test::isPrime);
	}

	static boolean isPrime(int num) {
		for (int i = 2; i <= num / 2; i++) {
			if (num % i == 0) return false;
		}
		return true;
	}

	// 测试foreach方法
	@Benchmark
	public void testForEach(){
		Test.foreach();
	}

	// 测试parallel方法
	@Benchmark
	public void testParallel() {
		Test.parallel();
	}
}

分别运行 testForEachtestParallel,会得到如下两个测试报告:

普通foreach获取质数的性能报告:

# JMH version: 1.21      --JMH版本
# VM version: JDK 1.8.0_112, Java HotSpot(TM) 64-Bit Server VM, 25.112-b15  --JDK,JVM信息
# VM invoker: D:\java\jdk1.8-64-win\jre\bin\java.exe  --JVM路径
# VM options: -Dfile.encoding=UTF-8  --JVM参数
# Warmup: 5 iterations, 10 s each  --预热操作,每隔10s循环调用方法五次完成预热
# Measurement: 5 iterations, 10 s each  -- 测试也是每隔10s调用方法五次完成测试
# Timeout: 10 min per iteration  --超时:如果每次迭代超过10分钟,就算超时了
# Threads: 1 thread, will synchronize iterations  -- 一个线程,进行同步迭代(可多个线程并发迭代)
# Benchmark mode: Throughput, ops/time  --Benchmark的模式为Throughput(吞吐量,即每秒操作次数,也就是此方法每秒执行多少次)
# Benchmark: com.yangsx95.test.Test.testForEach  --所测试的方法

# Run progress: 0.00% complete, ETA 00:08:20 --运行进度,开始时间
# Fork: 1 of 5  --第一次迭代,并且循环预热5此,循环调用5次
# Warmup Iteration   1: 0.865 ops/s --第一个10s预热
# Warmup Iteration   2: 0.854 ops/s --第二个10s
# Warmup Iteration   3: 0.871 ops/s
# Warmup Iteration   4: 0.872 ops/s
# Warmup Iteration   5: 0.872 ops/s
Iteration   1: 0.875 ops/s --开始测试,每秒大约调用foreach方法0.875次
Iteration   2: 0.874 ops/s
Iteration   3: 0.869 ops/s
Iteration   4: 0.872 ops/s
Iteration   5: 0.865 ops/s

# Run progress: 20.00% complete, ETA 00:06:56
# Fork: 2 of 5
# Warmup Iteration   1: 0.821 ops/s
# Warmup Iteration   2: 0.824 ops/s
# Warmup Iteration   3: 0.240 ops/s
# Warmup Iteration   4: 0.144 ops/s
# Warmup Iteration   5: 0.154 ops/s
Iteration   1: 0.385 ops/s
Iteration   2: 0.820 ops/s
Iteration   3: 0.821 ops/s
Iteration   4: 0.822 ops/s
Iteration   5: 0.824 ops/s

# Run progress: 40.00% complete, ETA 00:05:35
# Fork: 3 of 5
# Warmup Iteration   1: 0.865 ops/s
# Warmup Iteration   2: 0.863 ops/s
# Warmup Iteration   3: 0.862 ops/s
# Warmup Iteration   4: 0.861 ops/s
# Warmup Iteration   5: 0.862 ops/s
Iteration   1: 0.863 ops/s
Iteration   2: 0.863 ops/s
Iteration   3: 0.861 ops/s
Iteration   4: 0.865 ops/s
Iteration   5: 0.863 ops/s

# Run progress: 60.00% complete, ETA 00:03:39
# Fork: 4 of 5
# Warmup Iteration   1: 0.879 ops/s
# Warmup Iteration   2: 0.877 ops/s
# Warmup Iteration   3: 0.884 ops/s
# Warmup Iteration   4: 0.881 ops/s
# Warmup Iteration   5: 0.882 ops/s
Iteration   1: 0.883 ops/s
Iteration   2: 0.881 ops/s
Iteration   3: 0.878 ops/s
Iteration   4: 0.884 ops/s
Iteration   5: 0.883 ops/s

# Run progress: 80.00% complete, ETA 00:01:47
# Fork: 5 of 5
# Warmup Iteration   1: 0.840 ops/s
# Warmup Iteration   2: 0.832 ops/s
# Warmup Iteration   3: 0.826 ops/s
# Warmup Iteration   4: 0.829 ops/s
# Warmup Iteration   5: 0.830 ops/s
Iteration   1: 0.829 ops/s
Iteration   2: 0.825 ops/s
Iteration   3: 0.825 ops/s
Iteration   4: 0.825 ops/s
Iteration   5: 0.831 ops/s

--测试结果为
Result "com.yangsx95.test.Test.testForEach":
0.835 ±(99.9%) 0.073 ops/s [Average] --平均值为0.835,99.9%的波动在0.073内
(min, avg, max) = (0.385, 0.835, 0.884), stdev = 0.097 --最小、平均、最大吞吐量
CI (99.9%): [0.763, 0.908] (assumes normal distribution)

# Run complete. Total time: 00:08:59 --总共用时大约9分钟

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.

-- 测试结果,Cnt总共执行次数 5*5=25次,得分为 0.835, 波动在 ± 0.073 内
Benchmark          Mode  Cnt  Score   Error  Units
Test.testForEach  thrpt   25  0.835 ± 0.073  ops/s

1.8 Parallel 遍历获取质数的测试报告:

(这里由于篇幅的原因,只截取部分)

Benchmark           Mode  Cnt  Score   Error  Units
Test.testParallel  thrpt   25  4.946 ± 0.136  ops/s

可以看到 parallel 的吞吐量明显大于普通for循环的吞吐量。

JMH 基本概念

  • Warmup 预热,由于JVM中对于特定代码会存在优化(本地化),预热对于测试结果很重要

  • Mesurement 总共执行多少次测试

  • Benchmark mode 基准测试的模式

@BenchmarkMode

基准测试类型:

  • Throughput: 整体吞吐量,例如“1秒内可以执行多少次调用”。

  • AverageTime: 调用的平均时间,例如“每次调用平均耗时xxx毫秒”。

  • SampleTime: 随机取样,最后输出取样结果的分布,例如“99%的调用在xxx毫秒以内,99.99%的调用在xxx毫秒以内”

  • SingleShotTime: 以上模式都是默认一次 iteration 是 1s,唯有 SingleShotTime 是只运行一次。往往同时把 warmup 次数设为0,用于测试冷启动时的性能。

  • All(“all”, “All benchmark modes”);

@Warmup

一般我们前几次进行程序测试的时候都会比较慢, 所以要让程序进行几轮预热,保证测试的准确性。iterations就是预热轮数。time则是每次预热时间,batchSize:批处理大小,每次操作调用几次方法。

@Measurement

度量,其实就是一些基本的测试参数。

  • iterations 进行测试的轮次

  • time 每轮进行的时长

  • timeUnit 时长单位

都是一些基本的参数,可以根据具体情况调整。一般比较重的东西可以进行大量的测试,放到服务器上运行。

@Threads

每个进程中的测试线程,可用于类或者方法上。一般选择为cpu乘以2。如果配置了 Threads.MAX ,代表使用 Runtime.getRuntime().availableProcessors() 个线程。

@Fork

进行 fork 的次数。可用于类或者方法上。如果 fork 数是2的话,则 JMH 会 fork 出两个进程来进行测试。

@OutputTimeUnit

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

@Benchmark

方法级注解,表示该方法是需要进行 benchmark 的对象,用法和 JUnit 的 @Test 类似。

@Param

属性级注解,@Param 可以用来指定某项参数的多种情况。特别适合用来测试一个函数在不同的参数输入的情况下的性能。

@Setup

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

@TearDown

方法级注解,这个注解的作用就是我们需要在测试之后进行一些结束工作,比如关闭线程池,数据库连接等的,主要用于资源的回收等。

value值:

  • Trial:在每次Benchmark的之前/之后执行。

  • Iteration:在每次Benchmark的iteration的之前/之后执行。

  • Invocation:每次调用Benchmark标记的方法之前/之后都会执行。

@State

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

参考

最后更新于