性能调优之JMH必知必会3:编写正确的微基准测试用例


 

JMH必知必会系列文章(持续更新)

一、前言

 
  在前面两篇文章中分别介绍了什么是JMH、JMH的基本法。现在来介绍JMH正确的微基准测试用例如何编写。【单位换算:1秒(s)=1000000微秒(us)=1000000000纳秒(ns)
 
  官方JMH源码(包含样例,在jmh-samples包里)下载地址:https://github.com/openjdk/jmh/tags
 
  官方JMH样例在线浏览地址:http://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/
 
  本文内容参考书籍《Java高并发编程详解:深入理解并发核心库》,作者为 汪文君 ,读者有需要可以去购买正版书籍。
 
  本文由 @大白有点菜 原创,请勿盗用,转载请说明出处!如果觉得文章还不错,请点点赞,加关注,谢谢!
 

二、编写正确的微基准测试用例

1、添加JMH依赖包

  在Maven仓库中搜索依赖包jmh-corejmh-generator-annprocess ,版本为 1.36 。需要注释 jmh-generator-annprocess 包中的“<scope>test</scope>”,不然项目运行会报错。

<!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-core -->
<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-core</artifactId>
    <version>1.36</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-generator-annprocess -->
<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-generator-annprocess</artifactId>
    <version>1.36</version>
<!--            <scope>test</scope>-->
</dependency>

2、避免DCE(Dead Code Elimination)

  所谓Dead Code Elimination是指JVM为我们擦去了一些上下文无关,甚至经过计算之后确定压根不会用到的代码,如下面的代码块。

public void test(){
    
    
    int x=10;
    int y=10;
    int z=x+y;
}

  我们在test方法中分别定义了x和y,并且经过相加运算得到了z,但是在该方法的下文中再也没有其他地方使用到z(既没有对z进行返回,也没有对其进行二次使用,z甚至不是一个全局的变量),JVM很有可能会将test()方法当作一个空的方法来看待,也就是说会擦除对x、y的定义,以及计算z的相关代码。
 
【验证在Java代码的执行过程中虚拟机是否会擦除与上下文无关的代码 - 代码】

package cn.zhuangyt.javabase.jmh;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;

/**
 * JMH测试14:编写正确的微基准测试用例(避免DCE,即死码消除)
 * @author 大白有点菜
 */
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 5)
@Measurement(iterations = 5)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Thread)
public class JmhTestApp14_Coding_Correct_Benchmark_Case_DCE {
    
    

    @Benchmark
    public void baseline(){
    
    
        // 空的方法
    }

    @Benchmark
    public void measureLog1(){
    
    
        // 进行数学运算,但是在局部方法内
        Math.log(Math.PI);
    }

    @Benchmark
    public void measureLog2(){
    
    
        // result是通过数学运算所得并且在下一行代码中得到了使用
        double result = Math.log(Math.PI);
        // 对result进行数学运算,但是结果既不保存也不返回,更不会进行二次运算
        Math.log(result);
    }

    @Benchmark
    public double measureLog3(){
    
    
        // 返回数学运算结果
        return Math.log(Math.PI);
    }

    public static void main(String[] args) throws RunnerException {
    
    
        Options opt = new OptionsBuilder()
                .include(JmhTestApp14_Coding_Correct_Benchmark_Case_DCE.class.getSimpleName())
                .forks(1)
                .build();

        new Runner(opt).run();
    }
}

 
【验证在Java代码的执行过程中虚拟机是否会擦除与上下文无关的代码 - 代码运行结果】

Benchmark                                               Mode  Cnt   Score    Error  Units
JmhTestApp14_Coding_Correct_Benchmark_Case_DCE.baseline     avgt    510⁻⁴           us/op
JmhTestApp14_Coding_Correct_Benchmark_Case_DCE.measureLog1  avgt    510⁻⁴           us/op
JmhTestApp14_Coding_Correct_Benchmark_Case_DCE.measureLog2  avgt    510⁻⁴           us/op
JmhTestApp14_Coding_Correct_Benchmark_Case_DCE.measureLog3  avgt    5   0.002 ±  0.001  us/op
  • baseline 方法作为一个空的方法,主要用于做基准数据。
  • measureLog1 中虽然进行了 log 运算,但是结果既没有再进行二次使用,也没有进行返回。
  • measureLog2 中同样进行了 log 运算,虽然第一次的运算结果是作为第二次入参来使用的,但是第二次执行结束后也再没有对其有更进一步的使用。
  • measureLog3 方法与 measureLog1 的方法类似,但是该方法对运算结果进行了返回操作。

  从输出结果看出,measureLog1 和 measureLog2 方法的基准性能与 baseline 几乎完全一致,因此我们可以肯定这两个方法中的代码进行过擦除操作,这样的代码被称为 Dead Code(死代码,其他地方都没有用到的代码片段),而 measureLog3 则与上述两个方法不同,由于它对结果进行了返回,因此 Math.log(PI) 不会被认为它是 Dead Code,因此它将占用一定的CPU时间。
 
  得出结论:要想编写性能良好的微基准测试方法,不要让方法存在Dead Code,最好每一个基准测试方法都有返回值
 
【附上官方Dead Code样例(JMHSample_08_DeadCode) - 代码】

package cn.zhuangyt.javabase.jmh;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;

/**
 * JMH测试14:官方Dead Code样例
 * @author 大白有点菜
 */
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class JmhTestApp14_DeadCode {
    
    

    /**
     * The downfall of many benchmarks is Dead-Code Elimination (DCE): compilers
     * are smart enough to deduce some computations are redundant and eliminate
     * them completely. If the eliminated part was our benchmarked code, we are
     * in trouble.
     *
     * 许多基准测试的失败是死代码消除(DCE):编译器足够聪明,可以推断出一些计算是多余的,并完全消除它们。
     * 如果被淘汰的部分是我们的基准代码,我们就有麻烦了。
     *
     * Fortunately, JMH provides the essential infrastructure to fight this
     * where appropriate: returning the result of the computation will ask JMH
     * to deal with the result to limit dead-code elimination (returned results
     * are implicitly consumed by Blackholes, see JMHSample_09_Blackholes).
     *
     * 幸运的是,JMH 提供了必要的基础设施来在适当的时候解决这个问题:返回计算结果将要求 JMH 处理结果
     * 以限制死代码消除(返回的结果被黑洞隐式消耗,请参阅 JMHSample_09_Blackholes)。
     */

    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() {
    
    
        // do nothing, this is a baseline
    }

    @Benchmark
    public void measureWrong() {
    
    
        // This is wrong: result is not used and the entire computation is optimized away.
        compute(x);
    }

    @Benchmark
    public double measureRight() {
    
    
        // This is correct: the result is being used.
        return compute(x);
    }

    public static void main(String[] args) throws RunnerException {
    
    
        Options opt = new OptionsBuilder()
                .include(JmhTestApp14_DeadCode.class.getSimpleName())
                .forks(1)
                .build();

        new Runner(opt).run();
    }
}

 
【附上官方Dead Code样例(JMHSample_08_DeadCode) - 代码运行结果】

Benchmark                           Mode  Cnt   Score   Error  Units
JmhTestApp14_DeadCode.baseline      avgt    5   0.261 ± 0.027  ns/op
JmhTestApp14_DeadCode.measureRight  avgt    5  13.920 ± 0.591  ns/op
JmhTestApp14_DeadCode.measureWrong  avgt    5   0.266 ± 0.039  ns/op

 
【官方Dead Code样例(JMHSample_08_DeadCode)注解 - 谷歌和百度翻译互补】

  许多基准测试的失败是死代码消除(DCE):编译器足够聪明,可以推断出一些计算是多余的,并完全消除它们。如果被淘汰的部分是我们的基准代码,我们就有麻烦了。
 
  幸运的是,JMH 提供了必要的基础设施来在适当的时候解决这个问题:返回计算结果将要求 JMH 处理结果以限制死代码消除(返回的结果被黑洞隐式消耗,请参阅 JMHSample_09_Blackholes)。

3、使用Balckhole

  假设在基准测试方法中,需要将两个计算结果作为返回值,那么我们该如何去做呢?我们第一时间想到的可能是将结果存放到某个数组或者容器当中作为返回值,但是这种对数组或者容器的操作会对性能统计造成干扰,因为对数组或者容器的写操作也是需要花费一定的CPU时间的。
 
  JMH提供了一个 Blackhole(黑洞) 类,可以在不作任何返回的情况下避免 Dead Code 的发生,与Linux系统下的黑洞设备 /dev/null 非常相似。
 
【Blackhole样例 - 代码】

package cn.zhuangyt.javabase.jmh;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;

/**
 * JMH测试14:编写正确的微基准测试用例(使用Blackhole,即黑洞)
 * @author 大白有点菜
 */
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 5)
@Measurement(iterations = 5)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
public class JmhTestApp14_Coding_Correct_Benchmark_Case_Blackhole {
    
    

    double x1 = Math.PI;
    double x2 = Math.PI * 2;

    @Benchmark
    public double baseline()
    {
    
    
        // 不是Dead Code,因为对结果进行了返回
        return Math.pow(x1, 2);
    }

    @Benchmark
    public double powButReturnOne()
    {
    
    
        // Dead Code会被擦除
        Math.pow(x1, 2);
        // 不会被擦除,因为对结果进行了返回
        return Math.pow(x2, 2);
    }

    @Benchmark
    public double powThenAdd()
    {
    
    
        // 通过加法运算对两个结果进行了合并,因此两次的计算都会生效
        return Math.pow(x1, 2) + Math.pow(x2, 2);
    }

    @Benchmark
    public void useBlackhole(Blackhole hole)
    {
    
    
        // 将结果存放至black hole中,因此两次pow操作都会生效
        hole.consume(Math.pow(x1, 2));
        hole.consume(Math.pow(x2, 2));
    }

    public static void main(String[] args) throws RunnerException {
    
    
        Options opt = new OptionsBuilder()
                .include(JmhTestApp14_Coding_Correct_Benchmark_Case_Blackhole.class.getSimpleName())
                .forks(1)
                .build();

        new Runner(opt).run();
    }
}

 
【Blackhole样例 - 代码运行结果】

Benchmark                                                             Mode  Cnt  Score   Error  Units
JmhTestApp14_Coding_Correct_Benchmark_Case_Blackhole.baseline         avgt    5  2.126 ± 0.163  ns/op
JmhTestApp14_Coding_Correct_Benchmark_Case_Blackhole.powButReturnOne  avgt    5  2.065 ± 0.112  ns/op
JmhTestApp14_Coding_Correct_Benchmark_Case_Blackhole.powThenAdd       avgt    5  2.181 ± 0.151  ns/op
JmhTestApp14_Coding_Correct_Benchmark_Case_Blackhole.useBlackhole     avgt    5  3.748 ± 0.342  ns/op
  • baseline 方法中对 x1 进行了 pow 运算,之后返回,因此这个基准测试方法是非常合理的。
  • powButReturnOne 方法中的第一个 pow 运算仍然避免不了被当作 Dead Code 的命运,因此我们很难得到两次 pow 计算的方法耗时,但是对 x2 的 pow 运算会作为返回值返回,因此不是 Dead Code。
  • powThenAdd 方法就比较聪明,它同样会有返回值,两次 pow 操作也会被正常执行,但是由于采取的是加法运算,因此相加操作的CPU耗时也被计算到了两次 pow 操作中。
  • useBlackhole 方法中两次 pow 方法都会被执行,但是我们并没有对其进行返回操作,而是将其写入了 black hole 之中。

  输出结果表明,baseline 和 putButReturnOne 方法的性能几乎是一样的,powThenAdd 的性能相比前两个方法占用CPU的时间要稍微长一些,原因是该方法执行了两次 pow 操作。在 useBlackhole 中虽然没有对两个参数进行任何的合并操作,但是由于执行了 black hole 的 consume 方法,因此也会占用一定的CPU资源。虽然 blackhole 的 consume 方法会占用一定的CPU资源,但是如果在无返回值的基准测试方法中针对局部变量的使用都统一通过 blackhole 进行 consume ,那么就可以确保同样的基准执行条件,就好比拳击比赛时,对抗的拳手之间需要统一的体重量级一样。
 
  得出结论:Blackhole 可以帮助你在无返回值的基准测试方法中避免DC(Dead Code)情况的发生
 
【附上官方Blackhole样例(JMHSample_09_Blackholes) - 代码】

package cn.zhuangyt.javabase.jmh;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;

/**
 * JMH测试14:官方Blackhole样例
 * @author 大白有点菜
 */
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
public class JmhTestApp14_Balckhole {
    
    

    /**
     * Should your benchmark require returning multiple results, you have to
     * consider two options (detailed below).
     *
     * 如果您的基准测试需要返回多个结果,您必须考虑两个选项(详细信息如下)。
     *
     * NOTE: If you are only producing a single result, it is more readable to
     * use the implicit return, as in JMHSample_08_DeadCode. Do not make your benchmark
     * code less readable with explicit Blackholes!
     *
     * 注意:如果您只生成一个结果,使用隐式返回更具可读性,如 JMHSample_08_DeadCode。不要使用显式黑洞来降低基准代码的可读性!
     */

    double x1 = Math.PI;
    double x2 = Math.PI * 2;

    private double compute(double d) {
    
    
        for (int c = 0; c < 10; c++) {
    
    
            d = d * d / Math.PI;
        }
        return d;
    }

    /**
     * Baseline measurement: how much a single compute() costs.
     *
     * 基线测量:单个 compute() 的成本是多少。
     */

    @Benchmark
    public double baseline() {
    
    
        return compute(x1);
    }

    /**
     * While the compute(x2) computation is intact, compute(x1)
     * is redundant and optimized out.
     *
     * 虽然 compute(x2) 计算完好无损,但 compute(x1) 是多余的并经过优化。
     *
     */

    @Benchmark
    public double measureWrong() {
    
    
        compute(x1);
        return compute(x2);
    }

    /**
     * This demonstrates Option A:
     *
     * 这演示了选项 A:
     *
     * Merge multiple results into one and return it.
     * This is OK when is computation is relatively heavyweight, and merging
     * the results does not offset the results much.
     *
     * 将多个结果合并为一个并返回。当计算相对重量级时,这是可以的,并且合并结果不会抵消太多结果。
     */

    @Benchmark
    public double measureRight_1() {
    
    
        return compute(x1) + compute(x2);
    }

    /**
     * This demonstrates Option B:
     *
     * 这演示了选项 B:
     *
     * Use explicit Blackhole objects, and sink the values there.
     * (Background: Blackhole is just another @State object, bundled with JMH).
     * 
     * 使用明确的 Blackhole 对象,并将值下沉到那里。
     * (背景:Blackhole 只是另一个 @State 对象,与 JMH 捆绑在一起)。
     */

    @Benchmark
    public void measureRight_2(Blackhole bh) {
    
    
        bh.consume(compute(x1));
        bh.consume(compute(x2));
    }

    public static void main(String[] args) throws RunnerException {
    
    
        Options opt = new OptionsBuilder()
                .include(JmhTestApp14_Balckhole.class.getSimpleName())
                .forks(1)
                .build();

        new Runner(opt).run();
    }
}

 
【附上官方Blackhole样例(JMHSample_09_Blackholes) - 代码运行结果】

Benchmark                              Mode  Cnt   Score   Error  Units
JmhTestApp14_Balckhole.baseline        avgt    5  13.691 ± 0.661  ns/op
JmhTestApp14_Balckhole.measureRight_1  avgt    5  22.318 ± 1.664  ns/op
JmhTestApp14_Balckhole.measureRight_2  avgt    5  26.079 ± 3.079  ns/op
JmhTestApp14_Balckhole.measureWrong    avgt    5  13.276 ± 1.931  ns/op

 
【官方Dead Code样例(JMHSample_09_Blackholes)注解 - 谷歌和百度翻译互补】

  见代码中的注释

4、避免常量折叠(Constant Folding)

  常量折叠是Java编译器早期的一种优化——编译优化。在javac对源文件进行编译的过程中,通过词法分析可以发现某些常量是可以被折叠的,也就是可以直接将计算结果存放到声明中,而不需要在执行阶段再次进行运算。比如:

private final int x = 10;
private final int y = x*20;

  在编译阶段,y的值将被直接赋予200,这就是所谓的常量折叠。
 
【Constant Folding样例 - 代码】

package cn.zhuangyt.javabase.jmh;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;

/**
 * JMH测试14:编写正确的微基准测试用例(避免Constant Folding,即常量折叠)
 * @author 大白有点菜
 */
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 5)
@Measurement(iterations = 5)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
public class JmhTestApp14_Coding_Correct_Benchmark_Case_Constant_Folding {
    
    

    /**
     * x1和x2是使用final修饰的常量
     */
    private final double x1 = 124.456;
    private final double x2 = 342.456;

    /**
     * y1则是普通的成员变量
     */
    private double y1 = 124.456;
    /**
     * y2则是普通的成员变量
     */
    private double y2 = 342.456;

    /**
     * 直接返回124.456×342.456的计算结果,主要用它来作基准
     * @return
     */
    @Benchmark
    public double returnDirect()
    {
    
    
        return 42_620.703936d;
    }

    /**
     * 两个常量相乘,我们需要验证在编译器的早期优化阶段是否直接计算出了x1乘以x2的值
     * @return
     */
    @Benchmark
    public double returnCalculate_1()
    {
    
    
        return x1 * x2;
    }

    /**
     * 较为复杂的计算,计算两个未被final修饰的变量,主要也是用它来作为对比的基准
     * @return
     */
    @Benchmark
    public double returnCalculate_2()
    {
    
    
        return Math.log(y1) * Math.log(y2);
    }

    /**
     * 较为复杂的计算,操作的同样是final修饰的常量,查看是否在编译器优化阶段进行了常量的折叠行为
     * @return
     */
    @Benchmark
    public double returnCalculate_3()
    {
    
    
        return Math.log(x1) * Math.log(x2);
    }

    public static void main(String[] args) throws RunnerException {
    
    
        Options opt = new OptionsBuilder()
                .include(JmhTestApp14_Coding_Correct_Benchmark_Case_Constant_Folding.class.getSimpleName())
                .forks(1)
                .build();

        new Runner(opt).run();
    }
}

 
【Constant Folding样例 - 代码运行结果】

Benchmark                                                                      Mode  Cnt   Score   Error  Units
JmhTestApp14_Coding_Correct_Benchmark_Case_Constant_Folding.returnCalculate_1  avgt    5   1.873 ± 0.119  ns/op
JmhTestApp14_Coding_Correct_Benchmark_Case_Constant_Folding.returnCalculate_2  avgt    5  36.126 ± 2.372  ns/op
JmhTestApp14_Coding_Correct_Benchmark_Case_Constant_Folding.returnCalculate_3  avgt    5   1.888 ± 0.169  ns/op
JmhTestApp14_Coding_Correct_Benchmark_Case_Constant_Folding.returnDirect       avgt    5   1.869 ± 0.115  ns/op

  我们可以看到,1、3、4三个方法的统计数据几乎相差无几,这也就意味着在编译器优化的时候发生了常量折叠,这些方法在运行阶段根本不需要再进行计算,直接将结果返回即可,而第二个方法的统计数据就没那么好看了,因为早期的编译阶段不会对其进行任何的优化。
 
【附上官方Constant Folding样例(JMHSample_10_ConstantFold) - 代码】

package cn.zhuangyt.javabase.jmh;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;

/**
 * JMH测试14:官方Constant Folding样例
 * @author 大白有点菜
 */
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
public class JmhTestApp14_ConstantFolding {
    
    

    /**
     * The flip side of dead-code elimination is constant-folding.
     *
     * 死码消除的另一面是常量折叠。
     *
     * If JVM realizes the result of the computation is the same no matter what,
     * it can cleverly optimize it. In our case, that means we can move the
     * computation outside of the internal JMH loop.
     *
     * 如果 JVM 意识到无论如何计算的结果都是一样的,它可以巧妙地优化它。
     * 在我们的例子中,这意味着我们可以将计算移到内部 JMH 循环之外。
     *
     * This can be prevented by always reading the inputs from non-final
     * instance fields of @State objects, computing the result based on those
     * values, and follow the rules to prevent DCE.
     *
     * 这可以通过始终读取 @State 对象的非最终实例字段的输入,根据这些值计算结果,并遵循防止 DCE 的规则来防止。
     */

    // IDEs will say "Oh, you can convert this field to local variable". Don't. Trust. Them.
    // IDEs 会说“哦,你可以将这个字段转换为局部变量”。不要.相信.它们.
    // (While this is normally fine advice, it does not work in the context of measuring correctly.)
    // (虽然这通常是很好的建议,但它在正确测量的情况下不起作用。)
    private double x = Math.PI;

    // IDEs will probably also say "Look, it could be final". Don't. Trust. Them. Either.
    // IDEs 可能还会说“看,它可能是最终版本”。 也.不要.相信.它们.
    // (While this is normally fine advice, it does not work in the context of measuring correctly.)
    // (虽然这通常是很好的建议,但它在正确测量的情况下不起作用。)
    private final double wrongX = Math.PI;

    private double compute(double d) {
    
    
        for (int c = 0; c < 10; 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() {
    
    
        // This is wrong: the source is predictable, and computation is foldable.
        // 这是错误的:来源是可预测的,计算是可折叠的。
        return compute(Math.PI);
    }

    @Benchmark
    public double measureWrong_2() {
    
    
        // This is wrong: the source is predictable, and computation is foldable.
        // 这是错误的:来源是可预测的,计算是可折叠的。
        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(JmhTestApp14_ConstantFolding.class.getSimpleName())
                .forks(1)
                .build();

        new Runner(opt).run();
    }
}

 
【附上官方Constant Folding样例(JMHSample_10_ConstantFold) - 代码运行结果】

Benchmark                                    Mode  Cnt   Score   Error  Units
JmhTestApp14_ConstantFolding.baseline        avgt    5   1.871 ± 0.077  ns/op
JmhTestApp14_ConstantFolding.measureRight    avgt    5  13.989 ± 0.909  ns/op
JmhTestApp14_ConstantFolding.measureWrong_1  avgt    5   1.846 ± 0.075  ns/op
JmhTestApp14_ConstantFolding.measureWrong_2  avgt    5   1.870 ± 0.090  ns/op

 
【官方Constant Folding样例(JMHSample_10_ConstantFold)注解 - 谷歌和百度翻译互补】

  见代码中的注释

5、避免循环展开(Loop Unwinding)

  我们在编写JMH代码的时候,除了要避免Dead Code以及减少对常量的引用之外,还要尽可能地避免或者减少在基准测试方法中出现循环,因为循环代码在运行阶段(JVM后期优化)极有可能被“痛下杀手”进行相关的优化,这种优化被称为循环展开,下面我们来看一下什么是循环展开(Loop Unwinding)。

int sum=0;
for(int i = 0;i<100;i++){
    
    
    sum+=i;
}

  上面的例子中,sum=sum+i 这样的代码会被执行100次,也就是说,JVM会向CPU发送100次这样的计算指令,这看起来并没有什么,但是JVM的设计者们会认为这样的方式可以被优化成如下形式(可能)。

int sum=0;
for(int i = 0;i<20; i+=5){
    
    
    sum+=i;
    sum+=i+1;
    sum+=i+2;
    sum+=i+3;
    sum+=i+4;
}

  优化后将循环体中的计算指令批量发送给CPU,这种批量的方式可以提高计算的效率,假设1+2这样的运算执行一次需要1纳秒的CPU时间,那么在一个10次循环的计算中,我们觉得它可能是10纳秒的CPU时间,但是真实的计算情况可能不足10纳秒甚至更低。
 
【Loop Unwinding样例 - 代码】

package cn.zhuangyt.javabase.jmh;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;

/**
 * JMH测试14:编写正确的微基准测试用例(避免Loop Unwinding,即循环展开)
 * @author 大白有点菜
 */
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 5)
@Measurement(iterations = 5)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
public class JmhTestApp14_Coding_Correct_Benchmark_Case_Loop_Unwinding {
    
    

    private int x = 1;
    private int y = 2;

    @Benchmark
    public int measure()
    {
    
    
        return (x + y);
    }

    private int loopCompute(int times)
    {
    
    
        int result = 0;
        for (int i = 0; i < times; i++)
        {
    
    
            result += (x + y);
        }
        return result;
    }

    @OperationsPerInvocation
    @Benchmark
    public int measureLoop_1()
    {
    
    
        return loopCompute(1);
    }

    @OperationsPerInvocation(10)
    @Benchmark
    public int measureLoop_10()
    {
    
    
        return loopCompute(10);
    }

    @OperationsPerInvocation(100)
    @Benchmark
    public int measureLoop_100()
    {
    
    
        return loopCompute(100);
    }

    @OperationsPerInvocation(1000)
    @Benchmark
    public int measureLoop_1000()
    {
    
    
        return loopCompute(1000);
    }

    public static void main(String[] args) throws RunnerException {
    
    
        Options opt = new OptionsBuilder()
                .include(JmhTestApp14_Coding_Correct_Benchmark_Case_Loop_Unwinding.class.getSimpleName())
                .forks(1)
                .build();

        new Runner(opt).run();
    }
}

 
【Loop Unwinding样例 - 代码运行结果】

Benchmark                                                                   Mode  Cnt  Score   Error  Units
JmhTestApp14_Coding_Correct_Benchmark_Case_Loop_Unwinding.measure           avgt    5  2.038 ± 0.167  ns/op
JmhTestApp14_Coding_Correct_Benchmark_Case_Loop_Unwinding.measureLoop_1     avgt    5  2.112 ± 0.548  ns/op
JmhTestApp14_Coding_Correct_Benchmark_Case_Loop_Unwinding.measureLoop_10    avgt    5  0.226 ± 0.013  ns/op
JmhTestApp14_Coding_Correct_Benchmark_Case_Loop_Unwinding.measureLoop_100   avgt    5  0.026 ± 0.003  ns/op
JmhTestApp14_Coding_Correct_Benchmark_Case_Loop_Unwinding.measureLoop_1000  avgt    5  0.023 ± 0.002  ns/op

  上面的代码中,measure() 方法进行了 x+y 的计算,measureLoop_1() 方法与 measure() 方法几乎是等价的,也是进行了 x+y 的计算,但是 measureLoop_10() 方法对 result+=(x+y) 进行了10次这样的操作,其实说白了就是调用了10次 measure() 或者 loopCompute(times=1) 。但是我们肯定不能直接拿10次的运算和1次运算所耗费的CPU时间去做比较,因此 @OperationsPerInvocation(10) 注解的作用就是在每一次 对measureLoop_10() 方法进行基准调用的时候将op操作记为10次。
 
  通过JMH的基准测试我们不难发现,在循环次数多的情况下,折叠的情况也比较多,因此性能会比较好,说明JVM在运行期对我们的代码进行了优化。
 
【附上官方Loop Unwinding样例(JMHSample_11_Loops) - 代码】

package cn.zhuangyt.javabase.jmh;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;

/**
 * JMH测试14:官方Loop Unwinding样例
 * @author 大白有点菜
 */
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
public class JmhTestApp14_LoopUnwinding {
    
    

    /**
     * It would be tempting for users to do loops within the benchmarked method.
     * (This is the bad thing Caliper taught everyone). These tests explain why
     * this is a bad idea.
     *
     * 对于用户来说,在基准方法中进行循环是很有吸引力的。
     * (这是 Caliper 教给大家的坏事)。这些测试解释了为什么这是一个坏主意。
     *
     * Looping is done in the hope of minimizing the overhead of calling the
     * test method, by doing the operations inside the loop instead of inside
     * the method call. Don't buy this argument; you will see there is more
     * magic happening when we allow optimizers to merge the loop iterations.
     *
     * 循环是为了最小化调用测试方法的开销,通过在循环内而不是在方法调用内进行操作。
     * 不要相信这个论点; 当我们允许优化器合并循环迭代时,您会看到更多神奇的事情发生。
     */

    /**
     * Suppose we want to measure how much it takes to sum two integers:
     */

    int x = 1;
    int y = 2;

    /**
     * This is what you do with JMH.
     * 这是您使用JMH所做的。
     */

    @Benchmark
    public int measureRight() {
    
    
        return (x + y);
    }

    /**
     * The following tests emulate the naive looping.
     * 以下测试模拟了天真的循环。
     * This is the Caliper-style benchmark.
     * 这是 Caliper 风格的基准测试。
     */
    private int reps(int reps) {
    
    
        int s = 0;
        for (int i = 0; i < reps; i++) {
    
    
            s += (x + y);
        }
        return s;
    }

    /**
     * We would like to measure this with different repetitions count.
     * 我们想用不同的重复次数来衡量这一点。
     * Special annotation is used to get the individual operation cost.
     * 使用特殊注释来获得单个操作成本。
     */

    @Benchmark
    @OperationsPerInvocation(1)
    public int measureWrong_1() {
    
    
        return reps(1);
    }

    @Benchmark
    @OperationsPerInvocation(10)
    public int measureWrong_10() {
    
    
        return reps(10);
    }

    @Benchmark
    @OperationsPerInvocation(100)
    public int measureWrong_100() {
    
    
        return reps(100);
    }

    @Benchmark
    @OperationsPerInvocation(1_000)
    public int measureWrong_1000() {
    
    
        return reps(1_000);
    }

    @Benchmark
    @OperationsPerInvocation(10_000)
    public int measureWrong_10000() {
    
    
        return reps(10_000);
    }

    @Benchmark
    @OperationsPerInvocation(100_000)
    public int measureWrong_100000() {
    
    
        return reps(100_000);
    }

    public static void main(String[] args) throws RunnerException {
    
    
        Options opt = new OptionsBuilder()
                .include(JmhTestApp14_LoopUnwinding.class.getSimpleName())
                .forks(1)
                .build();

        new Runner(opt).run();
    }
}

 
【附上官方Loop Unwinding样例(JMHSample_11_Loops) - 代码运行结果】

Benchmark                                       Mode  Cnt  Score   Error  Units
JmhTestApp14_LoopUnwinding.measureRight         avgt    5  2.326 ± 0.089  ns/op
JmhTestApp14_LoopUnwinding.measureWrong_1       avgt    5  2.052 ± 0.085  ns/op
JmhTestApp14_LoopUnwinding.measureWrong_10      avgt    5  0.225 ± 0.006  ns/op
JmhTestApp14_LoopUnwinding.measureWrong_100     avgt    5  0.026 ± 0.001  ns/op
JmhTestApp14_LoopUnwinding.measureWrong_1000    avgt    5  0.022 ± 0.001  ns/op
JmhTestApp14_LoopUnwinding.measureWrong_10000   avgt    5  0.019 ± 0.001  ns/op
JmhTestApp14_LoopUnwinding.measureWrong_100000  avgt    5  0.017 ± 0.002  ns/op

 
【官方Loop Unwinding样例(JMHSample_11_Loops)注解 - 谷歌和百度翻译互补】

  见代码中的注释

6、Fork用于避免 配置文件引导的优化(profile-guided optimizations)

  Fork是用来干什么的呢?本节将会为大家介绍Fork的作用以及JVM的配置文件引导的优化(profile-guided optimizations)。
 
  在开始解释Fork之前,我们想象一下平时是如何进行应用性能测试的,比如我们要测试一下Redis分别在50、100、200个线程中同时进行共计一亿次的写操作时的响应速度,一般会怎样做?首先,我们会将Redis库清空,尽可能地保证每一次测试的时候,不同的测试用例站在同样的起跑线上,比如,服务器内存的大小、服务器磁盘的大小、服务器CPU的大小等基本上相同,这样的对比才是有意义的,然后根据测试用例对其进行测试,接着清理Redis服务器资源,使其回到测试之前的状态,最后统计测试结果做出测试报告。
 
  Fork的引入也是考虑到了这个问题,虽然Java支持多线程,但是不支持多进程,这就导致了所有的代码都在一个进程中运行,相同的代码在不同时刻的执行可能会引入前一阶段对进程profiler的优化,甚至会混入其他代码profiler优化时的参数,这很有可能会导致我们所编写的微基准测试出现不准确的问题。对于这种说法大家可能会觉得有些抽象,通过例子去理解更好。
 
【Fork设置为0样例 - 代码】

package cn.zhuangyt.javabase.jmh;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;

/**
 * JMH测试14:编写正确的微基准测试用例(Fork用于避免 profile-guided optimizations)
 * @author 大白有点菜
 */
@BenchmarkMode(Mode.AverageTime)
// 将Fork设置为0
@Fork(0)
// 将Fork设置为1
//@Fork(1)
@Warmup(iterations = 5)
@Measurement(iterations = 5)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Thread)
public class JmhTestApp14_Coding_Correct_Benchmark_Case_Fork {
    
    

    // Inc1 和Inc2的实现完全一样
    interface Inc
    {
    
    
        int inc();
    }

    public static class Inc1 implements Inc
    {
    
    
        private int i = 0;

        @Override
        public int inc()
        {
    
    
            return ++i;
        }
    }

    public static class Inc2 implements Inc
    {
    
    
        private int i = 0;

        @Override
        public int inc()
        {
    
    
            return ++i;
        }
    }

    private Inc inc1 = new Inc1();
    private Inc inc2 = new Inc2();

    private int measure(Inc inc)
    {
    
    
        int result = 0;
        for (int i = 0; i < 10; i++)
        {
    
    
            result += inc.inc();
        }
        return result;
    }

    @Benchmark
    public int measure_inc_1()
    {
    
    
        return this.measure(inc1);
    }

    @Benchmark
    public int measure_inc_2()
    {
    
    
        return this.measure(inc2);
    }

    @Benchmark
    public int measure_inc_3()
    {
    
    
        return this.measure(inc1);
    }

    public static void main(String[] args) throws RunnerException {
    
    
        Options opt = new OptionsBuilder()
                .include(JmhTestApp14_Coding_Correct_Benchmark_Case_Fork.class.getSimpleName())
                .build();

        new Runner(opt).run();
    }
}

 
【Fork设置为0样例 - 代码运行结果】

Benchmark                                                      Mode  Cnt  Score    Error  Units
JmhTestApp14_Coding_Correct_Benchmark_Case_Fork.measure_inc_1  avgt    5  0.002 ±  0.001  us/op
JmhTestApp14_Coding_Correct_Benchmark_Case_Fork.measure_inc_2  avgt    5  0.012 ±  0.001  us/op
JmhTestApp14_Coding_Correct_Benchmark_Case_Fork.measure_inc_3  avgt    5  0.012 ±  0.001  us/op

  将Fork设置为0,每一个基准测试方法都将会与 JmhTestApp14_Coding_Correct_Benchmark_Case_Fork 使用同一个JVM进程,因此基准测试方法可能会混入 JmhTestApp14_Coding_Correct_Benchmark_Case_Fork 进程的Profiler。
 
  measure_inc_1和 measure_inc_2 的实现方式几乎是一致的,它们的性能却存在着较大的差距,虽然 measure_inc_1 和 measure_inc_3 的代码实现完全相同,但还是存在着不同的性能数据,这其实就是JVM的 profiler-guided optimizations 导致的,由于我们所有的基准测试方法都与 JmhTestApp14_Coding_Correct_Benchmark_Case_Fork 的JVM进程共享,因此难免在其中混入 JmhTestApp14_Coding_Correct_Benchmark_Case_Fork 进程的Profiler,但是在将Fork设置为1的时候,也就是说每一次运行基准测试时都会开辟一个全新的JVM进程对其进行测试,那么多个基准测试之间将不会再存在干扰。
 
【Fork设置为1样例 - 代码】

package cn.zhuangyt.javabase.jmh;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;

/**
 * JMH测试14:编写正确的微基准测试用例(Fork用于避免 profile-guided optimizations)
 * @author 大白有点菜
 */
@BenchmarkMode(Mode.AverageTime)
// 将Fork设置为0
//@Fork(0)
// 将Fork设置为1
@Fork(1)
@Warmup(iterations = 5)
@Measurement(iterations = 5)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Thread)
public class JmhTestApp14_Coding_Correct_Benchmark_Case_Fork {
    
    

    // Inc1 和Inc2的实现完全一样
    interface Inc
    {
    
    
        int inc();
    }

    public static class Inc1 implements Inc
    {
    
    
        private int i = 0;

        @Override
        public int inc()
        {
    
    
            return ++i;
        }
    }

    public static class Inc2 implements Inc
    {
    
    
        private int i = 0;

        @Override
        public int inc()
        {
    
    
            return ++i;
        }
    }

    private Inc inc1 = new Inc1();
    private Inc inc2 = new Inc2();

    private int measure(Inc inc)
    {
    
    
        int result = 0;
        for (int i = 0; i < 10; i++)
        {
    
    
            result += inc.inc();
        }
        return result;
    }

    @Benchmark
    public int measure_inc_1()
    {
    
    
        return this.measure(inc1);
    }

    @Benchmark
    public int measure_inc_2()
    {
    
    
        return this.measure(inc2);
    }

    @Benchmark
    public int measure_inc_3()
    {
    
    
        return this.measure(inc1);
    }

    public static void main(String[] args) throws RunnerException {
    
    
        Options opt = new OptionsBuilder()
                .include(JmhTestApp14_Coding_Correct_Benchmark_Case_Fork.class.getSimpleName())
                .build();

        new Runner(opt).run();
    }
}

 
【Fork设置为1样例 - 代码运行结果】

Benchmark                                                      Mode  Cnt  Score    Error  Units
JmhTestApp14_Coding_Correct_Benchmark_Case_Fork.measure_inc_1  avgt    5  0.003 ±  0.001  us/op
JmhTestApp14_Coding_Correct_Benchmark_Case_Fork.measure_inc_2  avgt    5  0.003 ±  0.001  us/op
JmhTestApp14_Coding_Correct_Benchmark_Case_Fork.measure_inc_3  avgt    5  0.003 ±  0.001  us/op

  以上输出是将Fork设置为1的结果,是不是合理了很多,若将Fork设置为0,则会与运行基准测试的类共享同样的进程Profiler,若设置为1则会为每一个基准测试方法开辟新的进程去运行,当然,你可以将Fork设置为大于1的数值,那么它将多次运行在不同的进程中,不过一般情况下,我们只需要将Fork设置为1即可。
 
【附上官方Fork样例(JMHSample_12_Forking) - 代码】

package cn.zhuangyt.javabase.jmh;

import cn.zhuangyt.javabase.jmh.jmh_sample.JMHSample_12_Forking;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;

/**
 * JMH测试14:官方Forks样例
 * @author 大白有点菜
 */
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
public class JmhTestApp14_Fork {
    
    

    /**
     * JVMs are notoriously good at profile-guided optimizations. This is bad
     * for benchmarks, because different tests can mix their profiles together,
     * and then render the "uniformly bad" code for every test. Forking (running
     * in a separate process) each test can help to evade this issue.
     *
     * JVM 以擅长配置文件引导的优化而著称。 这对基准测试不利,因为不同的测试可以将它们的配置文件混合在一起,
     * 然后为每个测试呈现“一致糟糕”的代码。 分叉(在单独的进程中运行)每个测试可以帮助避免这个问题。
     *
     * JMH will fork the tests by default.
     *
     * JMH 将默认分叉测试。
     */

    /**
     * Suppose we have this simple counter interface, and two implementations.
     * Even though those are semantically the same, from the JVM standpoint,
     * those are distinct classes.
     *
     * 假设我们有这个简单的计数器接口和两个实现。尽管它们在语义上是相同的,但从 JVM 的角度来看,它们是不同的类。
     */

    public interface Counter {
    
    
        int inc();
    }

    public static class Counter1 implements JMHSample_12_Forking.Counter {
    
    
        private int x;

        @Override
        public int inc() {
    
    
            return x++;
        }
    }

    public static class Counter2 implements JMHSample_12_Forking.Counter {
    
    
        private int x;

        @Override
        public int inc() {
    
    
            return x++;
        }
    }

    /**
     * And this is how we measure it.
     * 这就是我们衡量它的方式。
     * Note this is susceptible for same issue with loops we mention in previous examples.
     * 请注意,这很容易受到我们在前面示例中提到的循环的相同问题的影响。
     */

    public int measure(JMHSample_12_Forking.Counter c) {
    
    
        int s = 0;
        for (int i = 0; i < 10; i++) {
    
    
            s += c.inc();
        }
        return s;
    }

    /**
     * These are two counters.
     */
    JMHSample_12_Forking.Counter c1 = new JMHSample_12_Forking.Counter1();
    JMHSample_12_Forking.Counter c2 = new JMHSample_12_Forking.Counter2();

    /**
     * We first measure the Counter1 alone...
     * 我们首先单独测量 Counter1 ...
     * Fork(0) helps to run in the same JVM.
     * Fork(0) 有助于在同一个 JVM 中运行。
     */

    @Benchmark
    @Fork(0)
    public int measure_1_c1() {
    
    
        return measure(c1);
    }

    /**
     * Then Counter2...
     * 然后到 Counter2...
     */

    @Benchmark
    @Fork(0)
    public int measure_2_c2() {
    
    
        return measure(c2);
    }

    /**
     * Then Counter1 again...
     * 然后再次到 Counter1
     */

    @Benchmark
    @Fork(0)
    public int measure_3_c1_again() {
    
    
        return measure(c1);
    }

    /**
     * These two tests have explicit @Fork annotation.
     * JMH takes this annotation as the request to run the test in the forked JVM.
     * It's even simpler to force this behavior for all the tests via the command
     * line option "-f". The forking is default, but we still use the annotation
     * for the consistency.
     *
     * 这两个测试有显示的 @Fork 注释。
     * JMH 将此注释作为在分叉的 JVM 中运行测试的请求。
     * 通过命令行选项“-f”为所有测试强制执行此行为甚至更简单。 分叉是默认的,但我们仍然使用注释来保持一致性。
     *
     * This is the test for Counter1.
     * 这是 Counter1 的测试
     */

    @Benchmark
    @Fork(1)
    public int measure_4_forked_c1() {
    
    
        return measure(c1);
    }

    /**
     * ...and this is the test for Counter2.
     * 还有这是 Counter2 的测试
     */

    @Benchmark
    @Fork(1)
    public int measure_5_forked_c2() {
    
    
        return measure(c2);
    }

    public static void main(String[] args) throws RunnerException {
    
    
        Options opt = new OptionsBuilder()
                .include(JmhTestApp14_Fork.class.getSimpleName())
                .build();

        new Runner(opt).run();
    }
}

 
【附上官方Fork样例(JMHSample_12_Forking) - 代码运行结果】

Benchmark                              Mode  Cnt   Score   Error  Units
JmhTestApp14_Fork.measure_1_c1         avgt    5   2.162 ± 0.129  ns/op
JmhTestApp14_Fork.measure_2_c2         avgt    5  12.490 ± 0.304  ns/op
JmhTestApp14_Fork.measure_3_c1_again   avgt    5  12.182 ± 0.605  ns/op
JmhTestApp14_Fork.measure_4_forked_c1  avgt    5   3.138 ± 0.162  ns/op
JmhTestApp14_Fork.measure_5_forked_c2  avgt    5   3.179 ± 0.302  ns/op

 
【官方Fork样例注解(JMHSample_12_Forking) - 谷歌和百度翻译互补】

  见代码中的注释

猜你喜欢

转载自blog.csdn.net/u014282578/article/details/128180516
今日推荐