醉卧草庐听风雨

君子藏器于身,待时而动

0%

JMH基准测试

2020年真是蝠如东海啊,开年就凸显出线上服务的性能问题,在手忙脚乱拆东墙补西墙的处理之后勉强算是稳定了下来,但随之而来的是对公司项目的优化和改进,然而改进了多少优化了多少,是需要量化的数据来表示,一般是使用例如jmeter之类的工具来量化数据,作为开发人员则可用JAVA大佬们写的基准测试工具JMH,其上手使用比较简单,下面就简单的介绍下它。

JMH介绍

JMH可以针对jvm程序的单个方法进行全方位的评估,通过简单的注解设置就可以进行性能测试,这里以一个例子为各位介绍下JMH,主要是对其注解做一些简单介绍,如果想要深入学习的话建议查阅官方的代码样例

举例说明

这里以json序列化为例,在此之前一直会有个疑问,序列化里jackson和fastjson哪个比较快?大家都说fastjson比较快,但是一直没有实际验证下,现在就用JMH来测测看是不是如此。先引入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>${jmh.version}</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>${jmh.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>

编写基准测试代码

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
@Threads(3) // 使用3个线程进行基准测试
@State(Scope.Benchmark) // 实例共享范围为所有基准测试线程
@Fork(value = 1, warmups = 1) // 预热1次,采样1次
@Warmup(iterations = 1) // 预热迭代1次
@Measurement(iterations = 1) // 采样迭代1次
@OutputTimeUnit(TimeUnit.MILLISECONDS) // 采样结果输出单位毫秒
@BenchmarkMode({Mode.AverageTime, Mode.Throughput}) // 每次执行平均耗时多少,单位时间内执行多少次
public class JsonBenchmark {
private Random random;
private ObjectMapper objectMapper;

@Setup // 基准测试之前执行
public void initParam() {
random = new Random();
objectMapper = new ObjectMapper();
}

@TearDown // 基准测试之后调用执行
public void release() {
objectMapper = null;
}

@Benchmark // 标识此方法进行基准测试
public void jackson() throws IOException {
Set<Integer> singleton = Collections.singleton(random.nextInt(100));
objectMapper.writeValueAsString(singleton);
}

@Benchmark // 标识此方法进行基准测试
public void fastjson() {
Set<Integer> singleton = Collections.singleton(random.nextInt(100));
JSON.toJSONString(singleton);
}

public static void main(String[] args) throws RunnerException {
Options options = new OptionsBuilder()
.include(JsonBenchmark.class.getSimpleName())
.build();
new Runner(options).run();
}
}

如上代码就完成了对json序列化的对比测试,可能一下子看是一头雾水,下面就来详细的介绍下例子里面的注解。

@Threads

注解在方法或类上,用来定义基准测试中多少个线程执行。例:@Threads(3),表示使用3个线程进行基准测试。

@Benchmark

仅能注解在方法上,被注解的方法将在基准测试中被执行并记录相关运行信息。

@OutputTimeUnit

可注解在类或方法上,表示基准测试的时间单位,一般用毫秒或秒。例:@OutputTimeUnit(TimeUnit.MILLISECONDS),以毫秒为单位统计。

@BenchmarkMode

基准测试模式,可注解在类或方法上,表示@Benchmark方法将被收集哪些数据,其包含以下几种模式:

  • Throughput:单位时间内方法执行的次数,基于迭代持续时间
  • AverageTime:单次方法执行的平均耗时,基于迭代持续时间
  • SampleTime:采样每次操作的时间,基于迭代持续时间
  • SingleShotTime:测量单次操作的时间,通常是用于冷启动测算方法的执行时间,使用较少
  • All:以上全部模式

以上介绍的这些注解组合起来用一句话概括就是:基准测试用几个线程以什么时间单位对哪些方法采集何种数据。

@Fork

执行次数,可注解在类或方法上,常用的是定义value和warmup值

  • value表示正式测量时运行次数,此数据会被记录
  • warmup表示正式测量前预热次数,预热表示运行但不进行数据统计

例:@Fork(value = 1, warmups = 3),预热3次后进行一次正式的采样。可能有人会问为什么会有个这个warmup,直接开测不行吗?这里就需要了解下jvm运行机制,一般情况下jvm是读取class字节码运行,但某些字节码被反复执行了之后,jvm就会动用jit对这部分字节码尝试编译成机器码,转换成机器码后执行效率会提升不少,所以使用warmup预热更能了解到被测方法的极限所在。

@Warmup

预热相关的参数控制,可注解在类或方法上,包含如下参数:

  • iterations:迭代次数
  • time:每次迭代的时长
  • timeUnit:时长单位
  • batchSize:每次操作中基准方法调用次数

例:@Warmup(iterations = 3, time = 10, timeUnit = TimeUnit.SECONDS, batchSize = 1),表示有三次迭代运行,每次持续10秒,每次操作调用一次基准方法

@Measurement

实际测量时相关参数,可注解在类或方法上,注解参数与@Warmup一样。

OperationsPerInvocation

随便提下这个注解,这个注解仅在处理循环类型的基准测试中才会使用

1
2
3
4
5
6
7
@Benchmark
@OperationsPerInvocation(10)
public void test() {
for (int i = 0; i < 10; i++) {
// do something
}
}

如上例子实际测试中1次调用有10次循环,但是JMH不清楚内部逻辑,在采样时仍认为是1次调用,为了校准数据加上@OperationsPerInvocation(10)来标识,这样JMH采样就知道这是10次调用。
关于以上这部分注解可以将其概括为:进行几次预热,几次采样,每次操作的处理方式。

@State

实例共享范围,只能注解在类上,目前有三种:

  • Benchmark:基准测试范围内所有线程均访问同一实例
  • Group:同一线程组访问同一实例,不同线程组有不同实例
  • Thread:每个线程拥有各自的实例,相互直接不共享

@Setup

仅可以注解在方法上,该方法在特定阶段之前执行,阶段包括以下三种:

  • Trial:基准测试前后,此为默认值
  • Iteration:每轮迭代前后
  • Invocation:每次方法调用前后

如示例中所写的那样,在基准测试前对jakcson的实例和用到的随机数生成器进行初始化。

@TearDown

@Setup类似不过是在特定阶段之后执行,一般用于连接的断开或是资源的释放,示例中只是做了一个简单置空操作。

@Param

这里提一个例子中没有的一个注解@Param,这个注解仅能注解在成员变量上,可以按序对成员变量进行赋值然后测试,需要和@State注解联合使用,字段类型有限必须即使基本类型和枚举类型。下面是官方的示例我简化了部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@State(Scope.Benchmark)
public class Params {

@Param({"1", "31", "65", "101", "103"})
public int arg;

@Param({"0", "1", "2", "4", "8", "16", "32"})
public int certainty;

@Benchmark
public boolean bench() {
return BigInteger.valueOf(arg).isProbablePrime(certainty);
}
}

当明确知道哪些参数要测试可以这么用,JMH会对每个参数依次进行赋值测试,所以执行周期会变长,上述例子将会进行5X7次的赋值测试。好了,以上这部分定义了基准测试中关于参数资源的一些动作,下面就来执行下。

结果分析

关于JMH的执行其实比较简单,无论你是打成jar还是直接在IDE里直接执行都可以,此处我直接在IDE中执行,最后得到如下结果:

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# JMH version: 1.21
# VM version: JDK 1.8.0_242, OpenJDK 64-Bit Server VM, 25.242-b20
# VM invoker: F:\Environment\jdk1.8.0.242\jre\bin\java.exe
# VM options: -javaagent:C:\Work\IDE\ideaIC\lib\idea_rt.jar=54743:C:\Work\IDE\ideaIC\bin -Dfile.encoding=UTF-8
# Warmup: 3 iterations, 10 s each
# Measurement: 3 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 3 threads, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: com.hst.redis.JsonBenchmark.fastjson

# Run progress: 0.00% complete, ETA 00:08:00
# Warmup Fork: 1 of 1
# Warmup Iteration 1: 13461.139 ops/ms
# Warmup Iteration 2: 13607.172 ops/ms
# Warmup Iteration 3: 13722.218 ops/ms
Iteration 1: 13705.623 ops/ms
Iteration 2: 13706.762 ops/ms
Iteration 3: 13700.618 ops/ms

# Run progress: 12.50% complete, ETA 00:07:12
# Fork: 1 of 1
# Warmup Iteration 1: 13576.412 ops/ms
# Warmup Iteration 2: 13103.999 ops/ms
# Warmup Iteration 3: 13561.005 ops/ms
Iteration 1: 13666.359 ops/ms
Iteration 2: 13684.048 ops/ms
Iteration 3: 13694.118 ops/ms


Result "com.hst.redis.JsonBenchmark.fastjson":
13681.508 ±(99.9%) 256.375 ops/ms [Average]
(min, avg, max) = (13666.359, 13681.508, 13694.118), stdev = 14.053
CI (99.9%): [13425.133, 13937.883] (assumes normal distribution)


# JMH version: 1.21
# VM version: JDK 1.8.0_242, OpenJDK 64-Bit Server VM, 25.242-b20
# VM invoker: F:\Environment\jdk1.8.0.242\jre\bin\java.exe
# VM options: -javaagent:C:\Work\IDE\ideaIC\lib\idea_rt.jar=54743:C:\Work\IDE\ideaIC\bin -Dfile.encoding=UTF-8
# Warmup: 3 iterations, 10 s each
# Measurement: 3 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 3 threads, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: com.hst.redis.JsonBenchmark.jackson

# Run progress: 25.00% complete, ETA 00:06:08
# Warmup Fork: 1 of 1
# Warmup Iteration 1: 8936.082 ops/ms
# Warmup Iteration 2: 9124.162 ops/ms
# Warmup Iteration 3: 9188.244 ops/ms
Iteration 1: 9015.721 ops/ms
Iteration 2: 9140.219 ops/ms
Iteration 3: 9194.396 ops/ms

# Run progress: 37.50% complete, ETA 00:05:06
# Fork: 1 of 1
# Warmup Iteration 1: 7456.722 ops/ms
# Warmup Iteration 2: 7484.857 ops/ms
# Warmup Iteration 3: 7290.126 ops/ms
Iteration 1: 7318.023 ops/ms
Iteration 2: 7315.458 ops/ms
Iteration 3: 7212.783 ops/ms


Result "com.hst.redis.JsonBenchmark.jackson":
7282.088 ±(99.9%) 1095.236 ops/ms [Average]
(min, avg, max) = (7212.783, 7282.088, 7318.023), stdev = 60.034
CI (99.9%): [6186.852, 8377.324] (assumes normal distribution)


# JMH version: 1.21
# VM version: JDK 1.8.0_242, OpenJDK 64-Bit Server VM, 25.242-b20
# VM invoker: F:\Environment\jdk1.8.0.242\jre\bin\java.exe
# VM options: -javaagent:C:\Work\IDE\ideaIC\lib\idea_rt.jar=54743:C:\Work\IDE\ideaIC\bin -Dfile.encoding=UTF-8
# Warmup: 3 iterations, 10 s each
# Measurement: 3 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 3 threads, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: com.hst.redis.JsonBenchmark.fastjson

# Run progress: 50.00% complete, ETA 00:04:05
# Warmup Fork: 1 of 1
# Warmup Iteration 1: ≈ 10⁻⁴ ms/op
# Warmup Iteration 2: ≈ 10⁻⁴ ms/op
# Warmup Iteration 3: ≈ 10⁻⁴ ms/op
Iteration 1: ≈ 10⁻⁴ ms/op
Iteration 2: ≈ 10⁻⁴ ms/op
Iteration 3: ≈ 10⁻⁴ ms/op

# Run progress: 62.50% complete, ETA 00:03:03
# Fork: 1 of 1
# Warmup Iteration 1: ≈ 10⁻⁴ ms/op
# Warmup Iteration 2: ≈ 10⁻⁴ ms/op
# Warmup Iteration 3: ≈ 10⁻⁴ ms/op
Iteration 1: ≈ 10⁻⁴ ms/op
Iteration 2: ≈ 10⁻⁴ ms/op
Iteration 3: ≈ 10⁻⁴ ms/op


Result "com.hst.redis.JsonBenchmark.fastjson":
≈ 10⁻⁴ ms/op


# JMH version: 1.21
# VM version: JDK 1.8.0_242, OpenJDK 64-Bit Server VM, 25.242-b20
# VM invoker: F:\Environment\jdk1.8.0.242\jre\bin\java.exe
# VM options: -javaagent:C:\Work\IDE\ideaIC\lib\idea_rt.jar=54743:C:\Work\IDE\ideaIC\bin -Dfile.encoding=UTF-8
# Warmup: 3 iterations, 10 s each
# Measurement: 3 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 3 threads, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: com.hst.redis.JsonBenchmark.jackson

# Run progress: 75.00% complete, ETA 00:02:02
# Warmup Fork: 1 of 1
# Warmup Iteration 1: ≈ 10⁻³ ms/op
# Warmup Iteration 2: ≈ 10⁻³ ms/op
# Warmup Iteration 3: ≈ 10⁻³ ms/op
Iteration 1: ≈ 10⁻³ ms/op
Iteration 2: ≈ 10⁻³ ms/op
Iteration 3: 0.001 ±(99.9%) 0.001 ms/op

# Run progress: 87.50% complete, ETA 00:01:01
# Fork: 1 of 1
# Warmup Iteration 1: ≈ 10⁻³ ms/op
# Warmup Iteration 2: ≈ 10⁻³ ms/op
# Warmup Iteration 3: ≈ 10⁻³ ms/op
Iteration 1: ≈ 10⁻³ ms/op
Iteration 2: ≈ 10⁻³ ms/op
Iteration 3: 0.001 ±(99.9%) 0.001 ms/op


Result "com.hst.redis.JsonBenchmark.jackson":
≈ 10⁻³ ms/op


# Run complete. Total time: 00:08:09

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
JsonBenchmark.fastjson thrpt 3 13681.508 ± 256.375 ops/ms
JsonBenchmark.jackson thrpt 3 7282.088 ± 1095.236 ops/ms
JsonBenchmark.fastjson avgt 3 ≈ 10⁻⁴ ms/op
JsonBenchmark.jackson avgt 3 ≈ 10⁻³ ms/op

完整的日志比较长,可以跳过中间执行部分,关注最后的结果就可以,主要看Score部分的数据

  • fastjson 每毫秒执行13681.508次,而jackson为7282.088次
  • fastjson 每次操作需10⁻⁴毫秒,而jackson为10⁻³毫秒

测试结果一目了然,之前还对fastjson还有所怀疑,现在看来确实挺快的。当然这只是我本机的运行之后的数据,每台电脑性能不同测出的数据也不一样,但这个数据是可以参考的。

写在最后

之前还想着性能测试如何量化,经过对JMH的学习了解之后这个问题就迎刃而解了,而且以后对任何性能有关的数据有怀疑的都可以直接用JMH测一测验证下,就比如json序列化哪家强这种问题,希望对各位有所帮助。