这里我们来操作官方的第五个例子,这里引出一个叫做固定方法的概念。
一、案例代码
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)
而且我们去掉了断言判断,因为一旦触发断言就会终止,你就只能看到一次。你能看到他疯狂调用。他是每一轮的每一次都会调。
所以你懂了吧。
五、总结
没啥总结,都写注释里面了。