以下内容在JVM server模式之下运行, JIT会被充分利用
Java语言非常智能, 有静态编译与动态编译的能力, 
会自己分析, 将无用的代码进行优化
在我们写JMH的时候, 这些自动优化的操作依然会进行
这样就带来一个问题:  我们要测试的代码如果被自动优化了, 很可能得到的测试结果与真实的结果差距会非常巨大
在我们写JMH的时候可能要写一些优化用例的代码, 这些优化并不是为了提高执行效率, 而是为了防止JVM自作主张的”优化”我们的代码, 因为我们就是为了测耗时的
1. 返回值未使用优化
在很多场景下, 如果我们的方法没有返回值, 计算过程可能都会被直接省略
但我们想测试执行效率, 就必须让方法真实执行
下面看个例子, 看看JVM是如何自动优化导致测试结果不准确的
1.1 完整示例
这里测试了一个空方法的耗时
@Warmup(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS)@Measurement(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS)@State(Scope.Thread)@BenchmarkMode(Mode.AverageTime)@OutputTimeUnit(TimeUnit.NANOSECONDS)public class Sample_08_DeadCode {private double x = Math.PI; //πprivate double compute(double d) {for (int c = 0; c < 10; c++) {d = d * d / Math.PI;}return d;}@Benchmarkpublic void baseline() {}@Benchmarkpublic void measureWrong() {compute(x);}@Benchmarkpublic double measureRight() {//正确的做法, 把结果返回, 让JVM认为计算不能省略return compute(x);}public static void main(String[] args) throws RunnerException {Options opt = new OptionsBuilder().include(Sample_08_DeadCode.class.getSimpleName()).forks(1).jvmArgs("-server") //注意这里一定要设置server模式, 以充分利用JIT.build();new Runner(opt).run();}}
测试报告:
Benchmark Mode Cnt Score Error UnitsSample_08_DeadCode.baseline avgt 0.278 ns/opSample_08_DeadCode.measureRight avgt 11.106 ns/opSample_08_DeadCode.measureWrong avgt 0.283 ns/op
1.2 被优化掉的方法
@Benchmarkpublic void baseline() {}//错误的例子, 这个方法会被JIT优化成空方法@Benchmarkpublic void measureWrong() {compute(x); //因为我们没有使用计算结果, JVM会直接把这段代码优化掉, 相当于基准测试了个空方法}
有时候JVM太智能了, 但我们在JMH里面并不想他们太智能;
发现没? measureWrong()方法明明写了耗时的计算代码, 居然被优化后耗时居然跟空方法几乎一样
这是因为JVM有JIT功能, 通过逃逸分析发现这个计算结果根本没有被使用, 在server模式下优化策略比较激进, 直接把没用的计算指令优化掉了
1.3 将计算结果返回防止优化
@Benchmarkpublic double measureRight() {//正确的做法, 把结果返回, 让JVM认为计算不能省略return compute(x);}
如果我们将计算结果当做返回值返回, 那么jvm就不会对计算过程进行优化, JVM会以为我们的计算是真正有用的
2. 使用黑洞
上面那种情况, 我们只有一个计算结果的时候, 直接返回就行了
但如果我们有2个计算结果, 返回也只能返回1个对象
另外一个没有返回的对象, 还是会被智能的JVM检测到进行优化
使得测试结果违背我们的预期
2.1 完整示例
此例请在JDK8之下使用
@Warmup(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS)@Measurement(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS)@BenchmarkMode(Mode.AverageTime)@OutputTimeUnit(TimeUnit.NANOSECONDS)@State(Scope.Thread)public class Sample_09_Blackholes {double x1 = Math.PI; //πdouble x2 = Math.PI * 2; //2π//基准@Benchmarkpublic double baseline() {return Math.log(x1); //求自然对数, 以e为底}//错误示例@Benchmarkpublic double measureWrong() {Math.log(x1); //编译器会自动识别, 在JIT的时候直接不执行, 使得JMH结果不准确return Math.log(x2); //真正有用的计算}//正确示例1@Benchmarkpublic double measureRight_1() {//所有计算结果都真实使用了return Math.log(x1) + Math.log(x2);}//正确示例2@Benchmarkpublic void measureRight_2(Blackhole bh) {//如果执行结果不使用编译器会自动优化//为了防止编译器自作主张, 这里使用JMH提供的黑洞Blackhole对象对执行结果进行消费bh.consume(Math.log(x1));bh.consume(Math.log(x2));}public static void main(String[] args) throws RunnerException {Options opt = new OptionsBuilder().include(Sample_09_Blackholes.class.getSimpleName()).forks(1).build();new Runner(opt).run();}}
测试报告:
Benchmark Mode Cnt Score Error UnitsSample_09_Blackholes.baseline avgt 18.827 ns/opSample_09_Blackholes.measureRight_1 avgt 34.445 ns/opSample_09_Blackholes.measureRight_2 avgt 37.923 ns/opSample_09_Blackholes.measureWrong avgt 18.180 ns/op
2.2 被优化掉的方法
double x1 = Math.PI; //πdouble x2 = Math.PI * 2; //2π//错误示例@Benchmarkpublic double measureWrong() {Math.log(x1); //编译器会自动识别, 在JIT的时候直接不执行, 使得JMH结果不准确return Math.log(x2); //真正有用的计算}
Math.log(x1)在这里计算自然对数的这行代码, 被编译器识别出来是无意义的计算, 就直接被优化掉了
真正执行JMH测试的时候, 并没有被执行
Benchmark Mode Cnt Score Error UnitsSample_09_Blackholes.baseline avgt 18.827 ns/opSample_09_Blackholes.measureWrong avgt 18.180 ns/op
2.3 通过返回值防止JVM自动优化
这里我们也可以用返回值的方式处理
double x1 = Math.PI; //πdouble x2 = Math.PI * 2; //2π//正确示例1@Benchmarkpublic double measureRight_1() {//所有计算结果都真实使用了return Math.log(x1) + Math.log(x2);}
但实际这种方式有个缺点, 它增加了一个计算返回值的操作, 在计算规模比较小的用例里, 可能会对JMH的结果有较大影响
2.4 使用黑洞
JMH还提供了一个方便的黑洞工具来给我们使用, 能方便的防止JVM自动优化
double x1 = Math.PI; //πdouble x2 = Math.PI * 2; //2π//正确示例2@Benchmarkpublic void measureRight_2(Blackhole bh) {//如果执行结果不使用编译器会自动优化//为了防止编译器自作主张, 这里使用JMH提供的黑洞Blackhole对象对执行结果进行消费bh.consume(Math.log(x1));bh.consume(Math.log(x2));}
直接将每个阶段的计算结果当做参数传入黑洞对象中
JVM就不会自作主张了
3. 处理常量折叠
Java不愧为后端使用最广泛的语言, 特性实在太多
编译器也很强大
比如这里, 它会在编译时进行分析, 如果
3.1 完整示例
@Warmup(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS)@Measurement(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS)@State(Scope.Thread)@BenchmarkMode(Mode.AverageTime)@OutputTimeUnit(TimeUnit.NANOSECONDS)public class Sample_10_ConstantFold {private double x = Math.PI;private final double wrongX = Math.PI;private double compute(double d) {for (int c = 0; c < 1000; c++) {d = d * d / Math.PI;}return d;}@Benchmarkpublic double baseline() {// simply return the value, this is a baselinereturn Math.PI;}@Benchmarkpublic double measureWrong_1() {//常量折叠了return compute(Math.PI);}@Benchmarkpublic double measureWrong_2() {//常量折叠了return compute(wrongX);}@Benchmarkpublic double measureRight() {// This is correct: the source is not predictable.return compute(x);}public static void main(String[] args) throws RunnerException {Options opt = new OptionsBuilder().include(Sample_10_ConstantFold.class.getSimpleName()).forks(1).jvmArgs("-server").build();new Runner(opt).run();}}
测试结果
Benchmark Mode Cnt Score Error UnitsSample_10_ConstantFold.baseline avgt 1.805 ns/opSample_10_ConstantFold.measureRight avgt 4798.269 ns/opSample_10_ConstantFold.measureWrong_1 avgt 2.318 ns/opSample_10_ConstantFold.measureWrong_2 avgt 2.493 ns/op
这差距恐怖不?
如果不用点手段防止JVM的自动优化,  那他妈测试结果就离谱
3.2 被优化掉的方法
private final double wrongX = Math.PI;private double compute(double d) {for (int c = 0; c < 1000; c++) {d = d * d / Math.PI;}return d;}@Benchmarkpublic double measureWrong_1() {//常量折叠了return compute(Math.PI);}@Benchmarkpublic double measureWrong_2() {//常量折叠了return compute(wrongX);}
你想想如果你是编译器, 在看到上面代码之后, 你是不是会想, 这他妈的有必要反复计算这个方法吗?
算一次直接把结果存下来就OK了, 因为每次计算结果都是100%可预测
所以真正在跑JMH的时候, 并不能真正衡量 compute()方法的性能
这种优化就叫做常量折叠, 在编译期直接就给你算好结果放那了
3.3 防止优化
上面会出现常量折叠的原因就是, 计算结果可预测
那么我们只要让计算结果不可预测, 就可以防止JVM自动优化了
最直接的方法就是, 让入参不是一个常量
private double x = Math.PI;@Benchmarkpublic double measureRight() {// This is correct: the source is not predictable.return compute(x);}
JVM经过一顿分析, 发现传入的参数x, 是个变量, 他的值现在是π, 但不能保证未来还是π, 所以就不敢对其进行优化
