HiSEN

JMH使用案例-日期格式化工具性能对比

一、背景

最近在看代码,发现一个 Date 格式化为 String 的方法。

1
2
3
public String dateFormatString() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(date);
}

看到这个方法想到

  • 每次都 new 一个 format 会快么(非线程安全,得每次都 new)?
  • SimpleDateFormat 格式化快么?
  • “yyyy-MM-dd HH:mm:ss.SSS” 改成静态常量会不会快点?

带着以上三个疑问,就想着做个对比测试。
恰巧最近在 perfma 社区看 jvm 相关内容时,刷到了『性能调优必备利器之 JMH』

优点:不用自己写相关统计代码,而且统计方式有多种

二、结论

性能从低到高

  • java.text.SimpleDateFormat
  • org.apache.commons.lang.time.DateFormatUtils
  • org.joda.time.DateTime 字符串静态常量影响
  • 是否静态常量几乎没有影响
  • 使用常量反而性能有所下降(???为何)
1
2
3
4
5
6
Benchmark                                             Mode  Cnt        Score        Error  Units
DateConvertTest.dateFormatString thrpt 10 867333.631 ± 9510.553 ops/s
DateConvertTest.dateFormatStringApache thrpt 10 2158217.955 ± 89012.866 ops/s
DateConvertTest.dateFormatStringApacheStaticPattern thrpt 10 2141167.550 ± 93715.889 ops/s
DateConvertTest.dateFormatStringJoda thrpt 10 2802803.925 ± 121600.833 ops/s
DateConvertTest.dateFormatStringJodaStaticPattern thrpt 10 2744918.391 ± 131925.235 ops/s

说明:
Error 列是空的,看 Score 和 Units 即可
ops/s:一秒钟执行多少次操作

三、代码

3.1 引入相关 jar

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.23</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.23</version>
</dependency>

3.2 IDEA 安装 JMH 插件

插件中心搜索:JMH
安装量最高的那个就是

3.3 编写代码

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
import org.apache.commons.lang.time.DateFormatUtils;
import org.joda.time.DateTime;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
* @author hisenyuan
* @date 2021/5/10 18:39
*/
// 默认的 State,每个测试线程分配一个实例
@State(Scope.Thread)
// 如果 fork 数是 2 的话,则 JMH 会 fork 出两个进程来进行测试。
@Fork(1)
// 预热的次数 3 次基准测试(不是执行三次方法)
@Warmup(iterations = 3)
// 基准执行次数 10 次(参数含义同上)
@Measurement(iterations = 10)
public class DateConvertTest {
private final static Date date = new Date();
private final static String PATTERN = "yyyy-MM-dd HH:mm:ss.SSS";

@Benchmark
public String dateFormatString() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(date);
}

@Benchmark
public String dateFormatStringJoda() {
return new DateTime(date).toString("yyyy-MM-dd HH:mm:ss.SSS");
}

@Benchmark
public String dateFormatStringJodaStaticPattern() {
return new DateTime(date).toString(PATTERN);
}

@Benchmark
public String dateFormatStringApache() {
return DateFormatUtils.format(date, "yyyy-MM-dd HH:mm:ss.SSS");
}

@Benchmark
public String dateFormatStringApacheStaticPattern() {
return DateFormatUtils.format(date, PATTERN);
}
}

3.4 执行

安装了 JMH 插件之后,直接对着类名右键,选择运行即可。
JMH 会默认执行当前类下面的所有的基准测试。

四、执行结果

ETA 00:10:50 代表整个测试需要近11分钟

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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# JMH version: 1.23
# VM version: JDK 1.8.0_171, Java HotSpot(TM) 64-Bit Server VM, 25.171-b11
# VM invoker: /Library/Java/JavaVirtualMachines/jdk1.8.0_171.jdk/Contents/Home/jre/bin/java
# VM options: -Dfile.encoding=UTF-8
# Warmup: 3 iterations, 10 s each
# Measurement: 10 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: com.jd.finance.jrpaypassword.performance.DateConvertTest.dateFormatString

# Run progress: 0.00% complete, ETA 00:10:50
# Fork: 1 of 1
# Warmup Iteration 1: 698627.190 ops/s
# Warmup Iteration 2: 873835.360 ops/s
# Warmup Iteration 3: 870972.646 ops/s
Iteration 1: 875650.632 ops/s
Iteration 2: 872407.599 ops/s
Iteration 3: 864390.061 ops/s
Iteration 4: 872456.756 ops/s
Iteration 5: 870538.248 ops/s
Iteration 6: 861600.599 ops/s
Iteration 7: 868469.312 ops/s
Iteration 8: 868930.572 ops/s
Iteration 9: 864686.616 ops/s
Iteration 10: 854205.911 ops/s


Result "com.jd.finance.jrpaypassword.performance.DateConvertTest.dateFormatString":
867333.631 ±(99.9%) 9510.553 ops/s [Average]
(min, avg, max) = (854205.911, 867333.631, 875650.632), stdev = 6290.642
CI (99.9%): [857823.077, 876844.184] (assumes normal distribution)


# JMH version: 1.23
# VM version: JDK 1.8.0_171, Java HotSpot(TM) 64-Bit Server VM, 25.171-b11
# VM invoker: /Library/Java/JavaVirtualMachines/jdk1.8.0_171.jdk/Contents/Home/jre/bin/java
# VM options: -Dfile.encoding=UTF-8
# Warmup: 3 iterations, 10 s each
# Measurement: 10 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: com.jd.finance.jrpaypassword.performance.DateConvertTest.dateFormatStringApache

# Run progress: 20.00% complete, ETA 00:08:42
# Fork: 1 of 1
# Warmup Iteration 1: 1922497.366 ops/s
# Warmup Iteration 2: 2121425.254 ops/s
# Warmup Iteration 3: 2142439.041 ops/s
Iteration 1: 2174791.808 ops/s
Iteration 2: 2101868.007 ops/s
Iteration 3: 2174003.026 ops/s
Iteration 4: 2202522.277 ops/s
Iteration 5: 2039234.570 ops/s
Iteration 6: 2105338.449 ops/s
Iteration 7: 2205933.974 ops/s
Iteration 8: 2175865.041 ops/s
Iteration 9: 2167544.736 ops/s
Iteration 10: 2235077.658 ops/s


Result "com.jd.finance.jrpaypassword.performance.DateConvertTest.dateFormatStringApache":
2158217.955 ±(99.9%) 89012.866 ops/s [Average]
(min, avg, max) = (2039234.570, 2158217.955, 2235077.658), stdev = 58876.499
CI (99.9%): [2069205.089, 2247230.821] (assumes normal distribution)


# JMH version: 1.23
# VM version: JDK 1.8.0_171, Java HotSpot(TM) 64-Bit Server VM, 25.171-b11
# VM invoker: /Library/Java/JavaVirtualMachines/jdk1.8.0_171.jdk/Contents/Home/jre/bin/java
# VM options: -Dfile.encoding=UTF-8
# Warmup: 3 iterations, 10 s each
# Measurement: 10 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: com.jd.finance.jrpaypassword.performance.DateConvertTest.dateFormatStringApacheStaticPattern

# Run progress: 40.00% complete, ETA 00:06:31
# Fork: 1 of 1
# Warmup Iteration 1: 1964208.975 ops/s
# Warmup Iteration 2: 2138997.654 ops/s
# Warmup Iteration 3: 2164194.409 ops/s
Iteration 1: 2221565.433 ops/s
Iteration 2: 2231776.725 ops/s
Iteration 3: 2210494.794 ops/s
Iteration 4: 2057487.199 ops/s
Iteration 5: 2111366.133 ops/s
Iteration 6: 2150355.985 ops/s
Iteration 7: 2116673.562 ops/s
Iteration 8: 2140376.847 ops/s
Iteration 9: 2099801.246 ops/s
Iteration 10: 2071777.579 ops/s


Result "com.jd.finance.jrpaypassword.performance.DateConvertTest.dateFormatStringApacheStaticPattern":
2141167.550 ±(99.9%) 93715.889 ops/s [Average]
(min, avg, max) = (2057487.199, 2141167.550, 2231776.725), stdev = 61987.258
CI (99.9%): [2047451.661, 2234883.440] (assumes normal distribution)


# JMH version: 1.23
# VM version: JDK 1.8.0_171, Java HotSpot(TM) 64-Bit Server VM, 25.171-b11
# VM invoker: /Library/Java/JavaVirtualMachines/jdk1.8.0_171.jdk/Contents/Home/jre/bin/java
# VM options: -Dfile.encoding=UTF-8
# Warmup: 3 iterations, 10 s each
# Measurement: 10 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: com.jd.finance.jrpaypassword.performance.DateConvertTest.dateFormatStringJoda

# Run progress: 60.00% complete, ETA 00:04:21
# Fork: 1 of 1
# Warmup Iteration 1: 2767967.530 ops/s
# Warmup Iteration 2: 2839342.269 ops/s
# Warmup Iteration 3: 2824726.959 ops/s
Iteration 1: 2859527.645 ops/s
Iteration 2: 2799624.402 ops/s
Iteration 3: 2876558.678 ops/s
Iteration 4: 2862478.579 ops/s
Iteration 5: 2882872.177 ops/s
Iteration 6: 2860788.081 ops/s
Iteration 7: 2742885.180 ops/s
Iteration 8: 2779727.212 ops/s
Iteration 9: 2719266.181 ops/s
Iteration 10: 2644311.112 ops/s


Result "com.jd.finance.jrpaypassword.performance.DateConvertTest.dateFormatStringJoda":
2802803.925 ±(99.9%) 121600.833 ops/s [Average]
(min, avg, max) = (2644311.112, 2802803.925, 2882872.177), stdev = 80431.422
CI (99.9%): [2681203.092, 2924404.757] (assumes normal distribution)


# JMH version: 1.23
# VM version: JDK 1.8.0_171, Java HotSpot(TM) 64-Bit Server VM, 25.171-b11
# VM invoker: /Library/Java/JavaVirtualMachines/jdk1.8.0_171.jdk/Contents/Home/jre/bin/java
# VM options: -Dfile.encoding=UTF-8
# Warmup: 3 iterations, 10 s each
# Measurement: 10 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: com.jd.finance.jrpaypassword.performance.DateConvertTest.dateFormatStringJodaStaticPattern

# Run progress: 80.00% complete, ETA 00:02:10
# Fork: 1 of 1
# Warmup Iteration 1: 2711358.480 ops/s
# Warmup Iteration 2: 2823432.725 ops/s
# Warmup Iteration 3: 2745545.759 ops/s
Iteration 1: 2752446.099 ops/s
Iteration 2: 2792047.450 ops/s
Iteration 3: 2828389.094 ops/s
Iteration 4: 2799921.909 ops/s
Iteration 5: 2670924.332 ops/s
Iteration 6: 2533779.871 ops/s
Iteration 7: 2733321.639 ops/s
Iteration 8: 2738839.592 ops/s
Iteration 9: 2802775.869 ops/s
Iteration 10: 2796738.055 ops/s


Result "com.jd.finance.jrpaypassword.performance.DateConvertTest.dateFormatStringJodaStaticPattern":
2744918.391 ±(99.9%) 131925.235 ops/s [Average]
(min, avg, max) = (2533779.871, 2744918.391, 2828389.094), stdev = 87260.375
CI (99.9%): [2612993.156, 2876843.626] (assumes normal distribution)


# Run complete. Total time: 00:10:52

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
DateConvertTest.dateFormatString thrpt 10 867333.631 ± 9510.553 ops/s
DateConvertTest.dateFormatStringApache thrpt 10 2158217.955 ± 89012.866 ops/s
DateConvertTest.dateFormatStringApacheStaticPattern thrpt 10 2141167.550 ± 93715.889 ops/s
DateConvertTest.dateFormatStringJoda thrpt 10 2802803.925 ± 121600.833 ops/s
DateConvertTest.dateFormatStringJodaStaticPattern thrpt 10 2744918.391 ± 131925.235 ops/s

五、JMH 介绍