如何保证微基准测试的正确性(避坑)
在实际应用中,微基准测试的适用性可能并不高。因为微基准测试的测试对象往往比较小。如,两个数据结构、两套算法、某个服务的两种不同实现方式。而为了尽量使测试结果对实际应用有更好的借鉴意义,我们不得不尽量将测试环境模拟得非常像真实环境。但这种费时蹩脚的模拟很可能不如直接将测试对象应用到真实环境中,然后对整个系统进行性能测试,所得结果就是真实环境的性能数据,而不用靠实验性的数据去“推测”真实环境性能。当然,在真实环境执行测试的成本与风险也不低。但通常总好过在满是漏洞的模拟环境中挣扎得出一个不靠谱的结果(完全浪费)。
(《解剖一个有缺陷的微基准测试》)
1. 从白盒层面理解代码,理解具体的性能开销
保证基准测试代码符合测试目的,即覆盖目标功能点,是前提。
同时还要了解被测代码在性能开销方面的不同点(包括CPU和内存)
2. 测试关注点应较少,以保证测试的有效性,也便于诊断问题
也意味着代码运行所需时间较少
3. 避免代码执行效率受 JVM 优化影响
Java 微基准测试需要非常了解 JVM 底层机制,避开一些 JVM 特性的影响,从而保证测试结果的准确性。
《解剖一个有缺陷的微基准测试》:https://www.ibm.com/developerworks/java/library/j-jtp02225/
3.1 保证代码经过了足够并且合适的预热
一段代码在 JVM 中被执行达到一定次数后,JIT 会将其编译为本地代码:
server 模式下是 10000 次;client 模式下是 1500 次
利用 -XX:+PrintCompilation 输出编译活动细节,可用于判断预热工作需要多久
参数 -Xbatch 可禁止JVM在后台编译代码。这样便于查看编译日志,以免和其它业务日志混在一起
当然,预热阶段的代码执行路径必须是和后续采集阶段的路径是相同的。否则就是没预热。
通过观察 PrintCompilation 的输出也可以判断测试后期是否存在不应出现的编译操作。
如果存在与预期不一致的编译行为,那么测试结果就是不准确的。
3.2 避免 JVM 消除无效代码
如果 JVM 检查某段代码执行与否不会对结果产生影响,它会自动忽略这部分代码。
Java代码
-
void method1() {
-
int a = 1;
-
int b = 2;
-
int c = a + b;
-
}
解决方法:尽量保持方法有返回值,或使用 JMH 的 Blackhole
Java代码
-
void method1(Blackhole blackhole) {
-
int a = 1;
-
int b = 2;
-
int c = a + b;
-
blackhole.consume(c);
-
}
3.3 防止常量折叠
如果 JVM 发现计算过程依赖于常量(或事实上的常量),它会跳过部分代码直接计算结果。
解决方法:使用 JMH 的 State 机制,将本地变量修改为 State 对象信息
Java代码
-
@State(Scope.Thread)
-
static class StateX {
-
public int a = 1;
-
public int b = 2;
-
}
-
-
void method1(StateX state, Blackhole blackhole) {
-
int a = state.a;
-
int b = state.b;
-
int c = a + b;
-
blackhole.consume(c);
-
}
3.4 避免 GC 操作影响测试准确性
Serial GC 和 Epsilon GC(JDK 11)是值得考虑的微基准测试 GC。
因为这两种 GC 所耗资源较少。
*3.5 方法内联对性能也会有影响
可添加启动参数 -XX:+PrintInlining 以便观察内联操作。
如果存在与预期不一致的内联操作,那么测试结果就是不准确的。