如何在JVM上做讲究一点的Benchmark
前些天看到了一个同事写的问题分析分享,他从服务的性能打点中得出Jackson性能比较差,并自己做实验得出了Jackson比org.json效率更低的结论。对他的代码稍做分析发现同事对写Benchmark还是有一些误解,结论分析也还是太过于草率。
其实做出有说服力的Benchmark还是需要一些工作的,也是比较严肃的。
本文仅限函数级的Benchmark。
1. Benchmarking
什么是Benchmarking?Benchmarking是有别于Testing的:
- Testing: 确保部分程序的表现是符合预期的(一般是二值输出,正确或者错误)
- Benchmarking: 计算部分程序的性能(Performance)指标(产出是个连续值,是个随机变量,要用统计方法对待它!)
影响程序性能的因素有很多,比如:
- 处理器的速度
- 处理器的核数(并发性能)
- 内存访问速度和吞吐量
- 缓存访问模式
- 程序运行时开销(垃圾回收GC,运行时编译JIT ,线程调度)
前三条并不是关注的重点,一般在评测的时候交待一下机器配置即可。4是在特殊的缓存访问模式下会有不同表现,比如经常需要刷新cache的值(cache失效)。5是程序启动时的额外开销,以及Full/Young GC带来的程序性能损耗,一般需要避开这些对Benchmark的影响。
怎样更好地做Benchmark呢?更详细的信息可以参考文献Statistically Rigorous Java Performance Evaluation。简单一点,首先要把性能指标当作随机变量,再用上一些简单的统计方法:
- 重复实验(比如10万+)
- 选用一些统计量(平均值,方差,分位点)
- 去掉异常值
- 确保稳定的状态(通过预热)
- 避免异常情况(GC,JIT)
为什么要预热Warmup?因为JVM程序在启动后需要经历一段时间的预热步骤,可能包括:
- 解释代码
- 部分程序被编译成机器码
- JVM还可能做一些额外优化
- 程序达到一个稳定状态
这些步骤会显著提升首次执行的时间,需要排除掉。
2. Json序列化/反序列化 Benchmark
上面的基本道理还是有必要了解的,从理念上提升理解,然后再借助一些实践这些方法论的工具,做Benchmark这件事就能比较好地完成了。
2.1 测试需要关注的点
回到Json序列化和反序列化的问题,有几点应该关注:
- 交待机器配置(一般只需要交待CPU和内存)
- 重复次数可观(10~100万)
- 区分Json(反)序列化是到Java Bean,还是完成相应库的内部结构表达
- org.json/net.sf-json是只能转换到内部结构的,而gson/jackson/fastjson是能直接转换到java bean的,只做对等能力的评测,这个很重要。
- 需要在不同的负载(字符串的大小)下进行测试
- 需要在简单的单层以及多层嵌套结构下分别测试
- 需要有基本的预热
2.2 使用测试框架
合理使用测试框架可以轻松完成2.1中提到的点,只是一点参数设置而已。
JMH is a Java harness for building, running, and analysing nano/micro/milli/macro benchmarks written in Java and other languages targetting the JVM.
OpenJDK发布的代码工具,不需要使用OpenJDK,加入下面的依赖即可使用。
"org.openjdk.jmh" % "jmh-generator-annprocess" % "1.21"
"org.openjdk.jmh" % "jmh-core" % "1.21"
如果是Scala的代码,也可以使用ScalaMeter。
ScalaMeter is a microbenchmarking and performance regression testing framework for the JVM platform that allows expressing performance tests in a way which is both simple and concise.
It can be used both from Scala and Java.
- write performance tests in a DSL similar to ScalaTest and ScalaCheck
- specify test input data
- specify how test re