以下内容在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;
}
@Benchmark
public void baseline() {
}
@Benchmark
public void measureWrong() {
compute(x);
}
@Benchmark
public 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 Units
Sample_08_DeadCode.baseline avgt 0.278 ns/op
Sample_08_DeadCode.measureRight avgt 11.106 ns/op
Sample_08_DeadCode.measureWrong avgt 0.283 ns/op
1.2 被优化掉的方法
@Benchmark
public void baseline() {
}
//错误的例子, 这个方法会被JIT优化成空方法
@Benchmark
public void measureWrong() {
compute(x); //因为我们没有使用计算结果, JVM会直接把这段代码优化掉, 相当于基准测试了个空方法
}
有时候JVM太智能了, 但我们在JMH里面并不想他们太智能;
发现没? measureWrong()
方法明明写了耗时的计算代码, 居然被优化后耗时居然跟空方法几乎一样
这是因为JVM有JIT功能, 通过逃逸分析发现这个计算结果根本没有被使用, 在server模式下优化策略比较激进, 直接把没用的计算指令优化掉了
1.3 将计算结果返回防止优化
@Benchmark
public 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π
//基准
@Benchmark
public double baseline() {
return Math.log(x1); //求自然对数, 以e为底
}
//错误示例
@Benchmark
public double measureWrong() {
Math.log(x1); //编译器会自动识别, 在JIT的时候直接不执行, 使得JMH结果不准确
return Math.log(x2); //真正有用的计算
}
//正确示例1
@Benchmark
public double measureRight_1() {
//所有计算结果都真实使用了
return Math.log(x1) + Math.log(x2);
}
//正确示例2
@Benchmark
public 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 Units
Sample_09_Blackholes.baseline avgt 18.827 ns/op
Sample_09_Blackholes.measureRight_1 avgt 34.445 ns/op
Sample_09_Blackholes.measureRight_2 avgt 37.923 ns/op
Sample_09_Blackholes.measureWrong avgt 18.180 ns/op
2.2 被优化掉的方法
double x1 = Math.PI; //π
double x2 = Math.PI * 2; //2π
//错误示例
@Benchmark
public double measureWrong() {
Math.log(x1); //编译器会自动识别, 在JIT的时候直接不执行, 使得JMH结果不准确
return Math.log(x2); //真正有用的计算
}
Math.log(x1)
在这里计算自然对数的这行代码, 被编译器识别出来是无意义的计算, 就直接被优化掉了
真正执行JMH测试的时候, 并没有被执行
Benchmark Mode Cnt Score Error Units
Sample_09_Blackholes.baseline avgt 18.827 ns/op
Sample_09_Blackholes.measureWrong avgt 18.180 ns/op
2.3 通过返回值防止JVM自动优化
这里我们也可以用返回值的方式处理
double x1 = Math.PI; //π
double x2 = Math.PI * 2; //2π
//正确示例1
@Benchmark
public 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
@Benchmark
public 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;
}
@Benchmark
public double baseline() {
// simply return the value, this is a baseline
return Math.PI;
}
@Benchmark
public double measureWrong_1() {
//常量折叠了
return compute(Math.PI);
}
@Benchmark
public double measureWrong_2() {
//常量折叠了
return compute(wrongX);
}
@Benchmark
public 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 Units
Sample_10_ConstantFold.baseline avgt 1.805 ns/op
Sample_10_ConstantFold.measureRight avgt 4798.269 ns/op
Sample_10_ConstantFold.measureWrong_1 avgt 2.318 ns/op
Sample_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;
}
@Benchmark
public double measureWrong_1() {
//常量折叠了
return compute(Math.PI);
}
@Benchmark
public double measureWrong_2() {
//常量折叠了
return compute(wrongX);
}
你想想如果你是编译器, 在看到上面代码之后, 你是不是会想, 这他妈的有必要反复计算这个方法吗?
算一次直接把结果存下来就OK了, 因为每次计算结果都是100%可预测
所以真正在跑JMH的时候, 并不能真正衡量 compute()
方法的性能
这种优化就叫做常量折叠, 在编译期直接就给你算好结果放那了
3.3 防止优化
上面会出现常量折叠的原因就是, 计算结果可预测
那么我们只要让计算结果不可预测, 就可以防止JVM自动优化了
最直接的方法就是, 让入参不是一个常量
private double x = Math.PI;
@Benchmark
public double measureRight() {
// This is correct: the source is not predictable.
return compute(x);
}
JVM经过一顿分析, 发现传入的参数x, 是个变量, 他的值现在是π, 但不能保证未来还是π, 所以就不敢对其进行优化