橘子学JDK之JMH-04(@Setup && @TearDown)

这里我们来操作官方的第五个例子,这里引出一个叫做固定方法的概念。

一、案例代码

package com.levi;

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;

/**
 * @Description:
 * @Author: Levi
 * @Date: 2024/4/9 10:09
 */
@State(Scope.Thread)// 线程享有
// 预热注解,修改为只预热一轮,每轮只跑一秒,默认是5,5这里改为1,1
@Warmup(iterations = 1,time = 1)
// 测试执行注解,修改为只执行一轮,每轮只跑一秒,默认是5,5这里改为1,1
@Measurement(iterations = 1,time = 1)
public class JMHSample_05_01_StateFixtures {

    // 成员变量初始化
    double x;

    /*
     * Since @State objects are kept around during the lifetime of the
     * benchmark, it helps to have the methods which do state housekeeping.
     * These are usual fixture methods, you are probably familiar with them from
     * JUnit and TestNG.
     * 由于@State对象在基准测试的生命周期内保持不变,因此有助于具有执行状态管理的方法。
     * 这些方法是常见的固定方法,您可能从JUnit和TestNG中熟悉它们。state对象在整个周期不变,
     * 我们可以在固定方法中操作他们。
     *
     * Fixture methods make sense only on @State objects, and JMH will fail to
     * compile the test otherwise.
     * 固定方法只对@State对象有意义,否则JMH将无法编译测试。下面的固定方法只操作state对象,别乱写
     * 因为我们是用的在我们这个类上面假的@State,所以他的变量就都是state变量。你要是写那种案例3里面
     * 的单独类变量,就得传进去state对象,不然编译报错。
     *
     * As with the State, fixture methods are only called by those benchmark
     * threads which are using the state. That means you can operate in the
     * thread-local context, and (not) use synchronization as if you are
     * executing in the context of benchmark thread.
     * 与状态一样,固定方法仅由使用该状态的基准线程调用。这意味着您可以在线程本地上下文中操作,并且(不)使用同步,
     * 就像您正在执行基准线程的上下文中一样。意思就是下面的固定方法是每个测试都会自己调,而且他们根据状态范围使用state变量
     * 所以你不需要显式的为他加同步机制。
     *
     * Note: fixture methods can also work with static fields, although the
     * semantics of these operations fall back out of State scope, and obey
     * usual Java rules (i.e. one static field per class).
     * 注意:固定方法也可以与静态字段一起工作,尽管这些操作的语义会回退到State范围之外,
     * 并遵守通常的Java规则(即每个类一个静态字段)。
      
      我总结一下,说句实话,我理解就是这些固定方法能像调用普通方法一样被调用,每个基准测试都会像来调用,而且他们各自调各自的,不存在同步之类的,这是人家的实现,再进一步说本例中的x作用域是Scope.Thread。首先他就是每个线程都自己操作自己的不共享,其次这些固定方法他们是独立在每个基准测试调用的,互相之间不存在竞争,各是各的。
     */

    /*
     * Ok, let's prepare our benchmark:这就是一个固定方法,每个Benchmark基准测试在执行之前都会调用它。你可以在这里
     * 对你的state对象做一些初始化等工作。
     */

    @Setup
    public void prepare() {
        // 这里我们加个输出,看看他调用的时机
        System.out.println("@Setup prepare is called!!!");
        // 对state变量做初始化
        x = Math.PI;
    }

    /*
     * And, check the benchmark went fine afterwards:
     * 这就是一个固定方法,每个Benchmark基准测试在执行结束之后都会调用它。你可以在这里
     * 对你的state对象做一些结束的检查或者是数据的释放等等。
     */

    @TearDown
    public void check() {
        // 这里我们加个输出,看看他调用的时机
        System.out.println("@TearDown check is called!!!");

        // 这是一句断言,表示如果x不等于pi的话,那就是通过检查,否则就抛出异常并且输出Nothing changed?
        // 因为x初始化就是pi,这里你等于了pi,那不就是没做修改吗。
        assert x != Math.PI : "Nothing changed?";
    }

    /*
     * This method obviously does the right thing, incrementing the field x
     * in the benchmark state. check() will never fail this way, because
     * we are always guaranteed to have at least one benchmark call.
     * 这个方法,对x做了自增,所以他最后是不等于pi的,会通过断言。
     */

    @Benchmark
    public void measureRight() {
        x++;
    }

    /*
     * This method, however, will fail the check(), because we deliberately
     * have the "typo", and increment only the local variable. This should
     * not pass the check, and JMH will fail the run.
     * 这个方法,对x又重新赋值了pi,所以他最后是等于pi的,不会通过断言,并且异常为啥也没做。
     */

    @Benchmark
    public void measureWrong() {
        x = Math.PI;
    }

    /*
     *
     * You can see measureRight() yields the result, and measureWrong() fires
     * the assert at the end of the run.
     * 测试结果你会看到measureRight会通过断言,因为他对x做了修改,他这个测试最后x是不等于pi的
     * 而measureWrong则无法通过断言
     *
     */

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(JMHSample_05_01_StateFixtures.class.getSimpleName())
                .forks(1)
                .jvmArgs("-ea")// 开启断言识别机制
                .build();

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

二、理解一下

上面的注释写的很详细了。但是我还想多比比一嘴。我理解的基准测试,就是我们有多个基准方法。然后呢,我们开启线程一个或者多个去调用。他的每一个基准方法都是独立的,也就是我们开启1个线程但是写了两个基准方法。那么他的流程其实是这个线程分别会去测试两个基准测试,而不是在他两之间切换,他们是独立的。所以每个基准对于state变量的操作他们之间是不影响的。
而你的阈值范围是Scope.Benchmark的时候也只是在某个基准测试里面多线程会竞争这个变量,其他基准里面是隔离的。

三、运行结果


# 这是第一个基准测试,我们看到他预热了一轮,执行了一轮,而且prepare 在开始预热之前执行了一次,check 在正式执行结束执行了一次,可见固定方法是跨越预热和测试的。不是预热前后各一次,执行前后各一次这样。
# Benchmark: com.levi.JMHSample_05_01_StateFixtures.measureRight
# Warmup Iteration   1: @Setup prepare is called!!!
442383548.197 ops/s
Iteration   1: @TearDown check is called!!!
464349180.322 ops/s


Result "com.levi.JMHSample_05_01_StateFixtures.measureRight":
  464349180.322 ops/s

# 这是第二个基准测试,我们能看到预热和执行也是执行了固定方法,但是他后面没通过断言,这个也是符合我们预期的。
# Benchmark mode: Throughput, ops/time
# Benchmark: com.levi.JMHSample_05_01_StateFixtures.measureWrong
# Warmup Iteration   1: @Setup prepare is called!!!
2045905663.479 ops/s
Iteration   1: @TearDown check is called!!!
<failure>

java.lang.AssertionError: Nothing changed?

我们再来看输出报告:
在这里插入图片描述
报告里面那个没通过断言的直接就没输出结果了,可见没通过断言的都不会被输出。

四、关于@Setup和@TearDown

其实这是官方的第六个案例,我们一样先把代码拉取下来。

package com.levi;
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;
/**
 * @Description:
 * @Author: Levi
 * @Date: 2024/4/9 11:20
 */
@State(Scope.Thread)
// 预热注解,修改为只预热一轮,每轮只跑一秒,默认是5,5这里改为1,1
@Warmup(iterations = 1,time = 1)
// 测试执行注解,修改为只执行一轮,每轮只跑一秒,默认是5,5这里改为1,1
@Measurement(iterations = 1,time = 1)
public class JMHSample_06_01_FixtureLevel {
    double x;

    /*
     * Fixture methods have different levels to control when they should be run.
     * There are at least three Levels available to the user. These are, from
     * top to bottom:
     * 固定法具有不同的级别来控制它们应该何时运行。用户至少可以使用三个级别。从上到下依次为:
     *
     * Level.Trial: before or after the entire benchmark run (the sequence of iterations)
     * Level.Iteration: before or after the benchmark iteration (the sequence of invocations)
     * Level.Invocation; before or after the benchmark method invocation (WARNING: read the Javadoc before using)
     * Level.Trial:在整个基准运行之前或之后
     * Level.Iteration:在基准迭代之前或之后
     * Level.Invocation:在基准方法调用之前或之后
     *
     *
     * Time spent in fixture methods does not count into the performance
     * metrics, so you can use this to do some heavy-lifting.
     * 在 固定方法中花费的时间不计入性能指标,因此您可以使用它来执行一些繁重的任务。
     */

    @TearDown(Level.Iteration)
    public void check() {
        System.out.println("tearDown被调用......");
        assert x > Math.PI : "Nothing changed?";
    }

    @Benchmark
    public void measureRight() {
        x++;
    }

    @Benchmark
    public void measureWrong() {
        double x = 0;
        x++;
    }

    /*
     * You can see measureRight() yields the result, and measureWrong() fires
     * the assert at the end of first iteration! This will not generate the results
     * for measureWrong(). You can also prevent JMH for proceeding further by
     * requiring "fail on error".
     */

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(JMHSample_06_01_FixtureLevel.class.getSimpleName())
                .forks(1)
                .jvmArgs("-ea")
                .shouldFailOnError(false) // switch to "true" to fail the complete run
                .build();

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

我们依然是先来看看代码,我们再开始使用这两个注解的时候说,他们是在基准测试的第一次调用(有预热就是预热)之前被调用,以及在测试结束之后调用。这就是他们调用的时机,但是他是默认的。我们这里一@TearDown为例子来看一下他们到底有什么调用时机,@Setup原理一样,我们就按照例子看一个即可。
我们先来看一下@TearDown注解的代码。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TearDown {

    /**
     * @return At which level to run this fixture.
     * @see Level
     */
    Level value() default Level.Trial;

}

我们看到他是用在方法上的,而且他内部有一个属性是一个Level的枚举。

public enum Level {

    /**
     * Trial level: to be executed before/after each run of the benchmark.
     *
     * <p>Trial is the set of benchmark iterations.</p>
     */
    Trial,

    /**
     * Iteration level: to be executed before/after each iteration of the benchmark.
     *
     * <p>Iteration is the set of benchmark invocations.</p>
     */
    Iteration,

    /**
     * Invocation level: to be executed for each benchmark method execution.
     *
     * <p><b>WARNING: HERE BE DRAGONS! THIS IS A SHARP TOOL.
     * MAKE SURE YOU UNDERSTAND THE REASONING AND THE IMPLICATIONS
     * OF THE WARNINGS BELOW BEFORE EVEN CONSIDERING USING THIS LEVEL.</b></p>
     *
     * <p>This level is only usable for benchmarks taking more than a millisecond
     * per single {@link Benchmark} method invocation. It is a good idea to validate
     * the impact for your case on ad-hoc basis as well.</p>
     *
     * <p>WARNING #1: Since we have to subtract the setup/teardown costs from
     * the benchmark time, on this level, we have to timestamp *each* benchmark
     * invocation. If the benchmarked method is small, then we saturate the
     * system with timestamp requests, which introduce artificial latency,
     * throughput, and scalability bottlenecks.</p>
     *
     * <p>WARNING #2: Since we measure individual invocation timings with this
     * level, we probably set ourselves up for (coordinated) omission. That means
     * the hiccups in measurement can be hidden from timing measurement, and
     * can introduce surprising results. For example, when we use timings to
     * understand the benchmark throughput, the omitted timing measurement will
     * result in lower aggregate time, and fictionally *larger* throughput.</p>
     *
     * <p>WARNING #3: In order to maintain the same sharing behavior as other
     * Levels, we sometimes have to synchronize (arbitrage) the access to
     * {@link State} objects. Other levels do this outside the measurement,
     * but at this level, we have to synchronize on *critical path*, further
     * offsetting the measurement.</p>
     *
     * <p>WARNING #4: Current implementation allows the helper method execution
     * at this Level to overlap with the benchmark invocation itself in order
     * to simplify arbitrage. That matters in multi-threaded benchmarks, when
     * one worker thread executing {@link Benchmark} method may observe other
     * worker thread already calling {@link TearDown} for the same object.</p>
     */
    Invocation,
}

我们根据注释来看一下每个枚举类型的含义:

Trial:在每次基准运行之前/之后执行,这个就是默认值,我们实际上之前的测试就看到了,他在你每个基准的开始和结束之后只执行一次,当然这个开始包括预热。

Iteration:在每次基准迭代之前/之后执行。当你设置为这个值的时候,就是每次执行都会被触发,假如你有三轮测试,那么就会执行三次。当然包括预热。这里说的是轮次,而不是你执行一轮,每轮里面跑一秒,这一秒可能跑几十万次,不是这个几十万次的每次都执行。

Invocation:这个得注释很长,而且官方说使用这个有风险,看一下javadoc注释,我们来翻译一下。
在每次基准方法执行之前/之后执行。
警告:此级别是一把双刃剑。在考虑使用此级别之前,请确保您理解其原因和潜在影响。
这个级别仅适用于每个单独方法调用需要超过一毫秒的基准。建议您根据实际情况验证其对您的影响。
警告1:由于我们必须从基准时间中减去设置/拆卸成本,因此在此级别上,我们必须为每个基准调用进行时间戳。如果被基准化的方法很小,那么我们会通过时间戳请求饱和系统,从而引入人为的延迟、吞吐量和可扩展性瓶颈。

警告2:由于我们使用此级别来测量单个调用的时间,可能会导致(协同)省略。这意味着测量中的故障可能会隐藏起来,从而产生令人惊讶的结果。例如,当我们使用时间来了解基准吞吐量时,被省略的时间测量将导致较低的聚合时间,从而产生虚构的更大吞吐量。

警告3:为了保持与其他级别相同的共享行为,我们有时必须在对对象的访问上进行同步(套期利润)。其他级别在测量之外执行此操作,但在此级别上,我们必须在关键路径上进行同步,进一步偏移测量。

警告4:当前实现允许在此级别上的辅助方法执行与基准调用本身重叠,以简化套期利润。这在多线程基准中很重要,当一个工作线程执行方法时,可能会观察到其他工作线程已经为相同对象调用。

这个风险很大,因为他就是真正的每次调用都会触发,可能你一轮跑几十亿次,他会调用几十亿次,这种不是JMH内部的实现和预测,是你自己的逻辑,比如你在里面初始化了一个线程池,那你可能会初始化几十亿次,这种非常危险,可能让你的机器瞬间卡死。

4.1、测试Trial级别

这个其实就是默认,不过我们还是测一下看看结果

package com.levi;
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;
/**
 * @Description:
 * @Author: Levi
 * @Date: 2024/4/9 11:20
 */
@State(Scope.Thread)
// 预热注解,修改为只预热一轮,每轮只跑一秒,默认是5,5这里改为1,1
@Warmup(iterations = 1,time = 1)
// 测试执行注解,修改为只执行一轮,每轮只跑一秒,默认是5,5这里改为1,1
@Measurement(iterations = 1,time = 1)
public class JMHSample_06_01_FixtureLevel {
    double x;

    @TearDown(Level.Trial)
    public void check() {
        System.out.println("tearDown被调用......");
        assert x > Math.PI : "Nothing changed?";
    }

    @Benchmark
    public void measureRight() {
        x++;
    }

    @Benchmark
    public void measureWrong() {
        // 这里修改的是本地变量了,不是state的x了,他自己又声明了一个
        double x = 0;
        x++;
    }


    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(JMHSample_06_01_FixtureLevel.class.getSimpleName())
                .forks(1)
                .jvmArgs("-ea")
                .shouldFailOnError(false) // switch to "true" to fail the complete run
                .build();

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

测试结果符合预期。

4.2、测试Iteration级别


package com.levi;
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;
/**
 * @Description:
 * @Author: Levi
 * @Date: 2024/4/9 11:20
 */
@State(Scope.Thread)
// 预热注解,修改为只预热一轮,每轮只跑一秒,默认是5,5这里改为1,1
@Warmup(iterations = 1,time = 1)
// 测试执行注解,修改为只执行一轮,每轮只跑一秒,默认是5,5这里改为1,1
@Measurement(iterations = 1,time = 1)
public class JMHSample_06_01_FixtureLevel {
    double x;

    @TearDown(Level.Iteration)
    public void check() {
        System.out.println("tearDown被调用......");
        assert x > Math.PI : "Nothing changed?";
    }

    @Benchmark
    public void measureRight() {
        x++;
    }

    @Benchmark
    public void measureWrong() {
        // 这里修改的是本地变量了,不是state的x了,他自己又声明了一个
        double x = 0;
        x++;
    }


    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(JMHSample_06_01_FixtureLevel.class.getSimpleName())
                .forks(1)
                .jvmArgs("-ea")
                .shouldFailOnError(false) // switch to "true" to fail the complete run
                .build();

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

符合预期,每轮执行都会触发,包括预热。

4.3、测试Invocation级别

package com.levi;
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;
/**
 * @Description:
 * @Author: Levi
 * @Date: 2024/4/9 11:20
 */
@State(Scope.Thread)
// 预热注解,修改为只预热一轮,每轮只跑一秒,默认是5,5这里改为1,1
@Warmup(iterations = 1,time = 1,timeUnit = TimeUnit.MILLISECONDS)
// 测试执行注解,修改为只执行一轮,每轮只跑一秒,默认是5,5这里改为1,1
@Measurement(iterations = 1,time = 1,timeUnit = TimeUnit.MILLISECONDS)
public class JMHSample_06_01_FixtureLevel {
    double x;

    @TearDown(Level.Invocation)
    public void check() {
        System.out.println("tearDown被调用......");
        // assert x > Math.PI : "Nothing changed?";
    }

    @Benchmark
    public void measureRight() {
        x++;
    }

    @Benchmark
    public void measureWrong() {
        // 这里修改的是本地变量了,不是state的x了,他自己又声明了一个
        double x = 0;
        x++;
    }


    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(JMHSample_06_01_FixtureLevel.class.getSimpleName())
                .forks(1)
                .jvmArgs("-ea")
                .shouldFailOnError(false) // switch to "true" to fail the complete run
                .build();

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

注意这里,因为这个是每次测试都会调,所以我把每轮跑的时间从1秒变为了1毫秒,不然我电脑实在顶不住。

// 预热注解,修改为只预热一轮,每轮只跑一秒,默认是5,5这里改为1,1
@Warmup(iterations = 1,time = 1,timeUnit = TimeUnit.MILLISECONDS)
// 测试执行注解,修改为只执行一轮,每轮只跑一秒,默认是5,5这里改为1,1
@Measurement(iterations = 1,time = 1,timeUnit = TimeUnit.MILLISECONDS)

而且我们去掉了断言判断,因为一旦触发断言就会终止,你就只能看到一次。你能看到他疯狂调用。他是每一轮的每一次都会调。
在这里插入图片描述
所以你懂了吧。

五、总结

没啥总结,都写注释里面了。

六、相关链接

JMH官方文档
JMH官方文档

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值