jmh用来对java以及其它运行在jvm上的语言进行基准测试,可以达到纳秒,微秒,毫秒,秒级别。
简介
通常简单的测试方法就是在调用一个方法前输出当前时间,调用后再次输出时间,多测试几次,但是这种方法是不准确的。在测试中会收到很多影响,比如jit的优化,cpu缓存,电源管理,超线程技术等等。因此需要有一款专业的基准测试框架。jmh会将这些影响降到最低。
使用
测试目标
举一个例子。
比较StringBuilder和StringBuffer的append函数的性能。
预测:StringBuild和StringBuffer的append()函数 ,StringBuffer在该方法上加了synchronize关键字,预测效率会比较低。
环境配置
我的电脑配置(有点差):
系统:         macOS 10.14.5
处理器名称:    Intel Core i5
处理器速度:    2.7 GHz
处理器数目:    1
核总数:    2
L2 缓存(每个核):    256 KB
L3 缓存:    3 MB
内存:    8 GB
超线程技术:  以启用
IDE: IDEA 2019.2.3
VM : JDK 1.8.0_231, Java HotSpot(TM) 64-Bit Server VM, 25.231-b11
测试
- 添加依赖
| 1 | <properties> | 
- 编写代码: - 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- package com.gentlezuo; 
 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.concurrent.TimeUnit;
 (Mode.All)
 (iterations = 3)
 (iterations = 3, time = 4, timeUnit = TimeUnit.SECONDS)
 (4)
 (1)
 (TimeUnit.MILLISECONDS)
 public class BuilderVSBuffer {
 
 public void builder() {
 StringBuilder sb = new StringBuilder();
 for (int i = 0; i < 100000; i++) {
 sb.append("i");
 }
 }
 
 public void buffer() {
 StringBuffer sb = new StringBuffer();
 for (int i = 0; i < 100000; i++) {
 sb.append("i");
 }
 }
 public static void main(String[] args) throws RunnerException {
 Options options = new OptionsBuilder()
 .include(BuilderVSBuffer.class.getSimpleName())
 .output("/Users/gentlezuo/logs/BuilderVSBuffer.log")
 .build();
 new Runner(options).run();
 }
 }
- 观察结果 
运行结束后会生成该日志文件,文件内容太多,只截取最后几行: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
Benchmark                                  Mode    Cnt   Score    Error   Units
BuilderVSBuffer.buffer                    thrpt      3   2.757 ±  6.092  ops/ms
BuilderVSBuffer.builder                   thrpt      3   3.170 ±  3.842  ops/ms
BuilderVSBuffer.buffer                     avgt      3   1.396 ±  0.449   ms/op
BuilderVSBuffer.builder                    avgt      3   1.191 ±  0.663   ms/op
BuilderVSBuffer.buffer                   sample  35191   1.357 ±  0.019   ms/op
BuilderVSBuffer.buffer:buffer·p0.00      sample          0.627            ms/op
BuilderVSBuffer.buffer:buffer·p0.50      sample          1.155            ms/op
BuilderVSBuffer.buffer:buffer·p0.90      sample          1.481            ms/op
BuilderVSBuffer.buffer:buffer·p0.95      sample          2.103            ms/op
BuilderVSBuffer.buffer:buffer·p0.99      sample          6.268            ms/op
BuilderVSBuffer.buffer:buffer·p0.999     sample         15.555            ms/op
BuilderVSBuffer.buffer:buffer·p0.9999    sample         29.378            ms/op
BuilderVSBuffer.buffer:buffer·p1.00      sample         31.785            ms/op
BuilderVSBuffer.builder                  sample  38499   1.239 ±  0.016   ms/op
BuilderVSBuffer.builder:builder·p0.00    sample          0.565            ms/op
BuilderVSBuffer.builder:builder·p0.50    sample          1.094            ms/op
BuilderVSBuffer.builder:builder·p0.90    sample          1.307            ms/op
BuilderVSBuffer.builder:builder·p0.95    sample          1.792            ms/op
BuilderVSBuffer.builder:builder·p0.99    sample          4.678            ms/op
BuilderVSBuffer.builder:builder·p0.999   sample         12.927            ms/op
BuilderVSBuffer.builder:builder·p0.9999  sample         33.594            ms/op
BuilderVSBuffer.builder:builder·p1.00    sample         46.727            ms/op
BuilderVSBuffer.buffer                       ss      3   2.699 ± 18.377   ms/op
BuilderVSBuffer.builder                      ss      3   1.605 ±  3.475   ms/op
直接看最后的结论:
- 吞吐量:builder是 3.170 ± 3.842 ops/ms;buffer是 2.757 ± 6.092,builder的吞吐量更高
- 平均耗时:builder依旧表现更好
- SampleTime,采样:比如p0.999表示满足99.9%的情况的耗时;可以看见在每一个分位值下builder都表现的更好
- 只调用一次:builder的表现更好
- 结论:StringBuilder的效率好,因此在不需要线程同步的时候,应该选择StringBuilder。
更多请参考官方的例子。
注解
@BenchmarkMode
基准测试的模式,有四种值
- Throughput(“thrpt”, “Throughput, ops/time”), 吞吐量,每一个时间单元执行的次数
- AverageTime(“avgt”, “Average time, time/op”), 每个操作的的平均时间
- SampleTime(“sample”, “Sampling time”), 随机取样,会给出满足百分之多少的情况的最坏的时间
- SingleShotTime(“ss”, “Single shot invocation time”), SingleShotTime 只运行一次。往往同时把 warmup 次数设为0,用于测试冷启动时的性能。
- All(“all”, “All benchmark modes”),以上所有都测试,最常用
Warmup
预热,由于JIT的存在,因此预热后的数据更平稳
Measurement
测试的一些基本的参数
- iterations:测试的次数
- time:每一次测试的时间
- TimeUnit:时间单位
- 每一个op调用方法的个数
Threads
测试的线程数,可以注解在类上,也可以在方法上
Fork
会fork出新的几个java虚拟机减少影响,需要设置一系列的参数
outputTimeUnit
基准测试的时间类型
@Benchmark
方法级的注解,每一个要测试的方法。
Param
属性级注解,可以用来指定某项参数的多种情况,在一个函数需要测试多组参数的时候很有用。
Setup
方法级注解,在测试之前做一些准备工作
TearDown
方法级注解,在测试之后进行一些结束工作
State
设置一个类在测试的时候在线程间的共享状态:
- Thread: 线程私有
- Group: 同一个组里面所有线程共享
- benchmark: 所有线程共享
总结
jmh是一款优秀的测试框架,但是测试的结果依旧依赖不同的环境,不可只测一次就相信。
参考:
https://www.xncoding.com/2018/01/07/java/jmh.html
http://openjdk.java.net/projects/code-tools/jmh/
