软件开发中,除要写出正确的代码之外,还需要写出高效的代码。这在并发编程中更加重要,原因主要有两点。首先,一部分并发程序由串行程序改造而来,其目的就是提高系统性能,因此,自然需要有一种方法对两种算法进行性能比较。其次,由于业务原因引入的多线程有可能因为线程并发控制导致性能损耗,因此要评估损耗的比重是否可以接受。无论出自何种原因需要进行性能评估,量化指标总是必要的。在大部分场合,简单地回答谁快谁慢是远远不够的,如何将程序性能量化呢? 这就是本节要介绍的 Java 微基准测试框架JMH。
传统的性能测试,一般会在方法前后打印时间戳,然后通过时间差来判断执行的耗时。
public static void dealHelloWorld() throws InterruptedException {
// 这里模拟该方法执行
Thread.sleep(1000);
}
public static void main(String[] args) throws InterruptedException {
long start0 = System.currentTimeMillis();
dealHelloWorld();
long end0 = System.currentTimeMillis();
System.out.println("执行耗时:" + (end0-start0) + "ms");
}
执行结果:
但是如果代码量较大,而且较为复杂的话,通常需要打印较多的时间戳,然后分段进行计算。就像这样:
这样的话,一方面业务代码中会融入很多的计算时间的代码,增加代码可阅读性;另一方面由于JVM可能会对代码进行运行时优化,比如循环展开、运行时编译等,这样会导致某组未经优化的性能数据参与统计计算。那么这时候就需要JMH了。
JMH(Java Microbenchmark Harness)是Java语言的微基准测试框架,用于准确、可靠地测量和评估Java代码的性能。它是由OpenJDK团队开发的,专门针对Java应用程序的性能测试和基准测试。通过JMH 可以对多个方法的性能进行定量分析。比如,当要知道执行一个函数需要多少时间,或者当对一个算法有多种不同实现时,需要选取性能最好的那个。
JMH官网地址:OpenJDK: jmh
Github地址:https://github.com/openjdk/jmh/tags
我们先来简单尝试使用一下。要使用JMH测试很简单,我们可以联想一下Junit单元测试步骤:
同样的,JMH也是这些步骤,只是依赖包些许不同。
<dependencies>
<!-- JMH核心代码 -->
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.35</version>
</dependency>
<!-- JMH注解相关依赖 -->
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.35</version>
</dependency>
</dependencies>
/**
* @author Shamee loop
* @date 2023/7/1
*/
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.SECONDS)
public class JMHTestHello01 {
/**
* @Benchmark 类似于Junit,表示被度量代码标注
*/
@Benchmark
public void dealHelloWorld() throws InterruptedException {
// 这里模拟该方法执行
Thread.sleep(1000);
}
public static void main(String[] args) throws RunnerException {
Options options = new OptionsBuilder()
.include(JMHTestHello01.class.getSimpleName())
.warmupIterations(3) // 预热的次数, 3次
.warmupTime(TimeValue.seconds(2)) // 预热的时间,2s
.forks(1) // 测试的执行线程数量
.build();
new Runner(options).run();
}
}
执行结果:
# ...... 这里省略部分信息,这些都是描述JDK和JMH的基础信息,基本信息等同于当下的环境以及option中的配置
# Benchmark: org.shamee.jmh.demo.JMHTestHello01.dealHelloWorld
# Run progress: 0.00% complete, ETA 00:00:56
# Fork: 1 of 1
# ...... 这里开始预热测试,我们指定了预热3次
# Warmup Iteration 1: 1.005 s/op
# Warmup Iteration 2: 1.010 s/op
# Warmup Iteration 3: 1.007 s/op
# ...... 这里迭代测试进行了5次,以及每次的时间
Iteration 1: 1.007 s/op
Iteration 2: 1.011 s/op
Iteration 3: 1.012 s/op
Iteration 4: 1.008 s/op
Iteration 5: 1.007 s/op
Result "org.shamee.jmh.demo.JMHTestHello01.dealHelloWorld":
1.009 ±(99.9%) 0.009 s/op [Average]
(min, avg, max) = (1.007, 1.009, 1.012), stdev = 0.002
CI (99.9%): [1.000, 1.018] (assumes normal distribution)
# Run complete. Total time: 00:00:59
REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.
# ...... 这里显示的汇总结果,cnt 执行了5次 score最后的结果 Error误差±0.009s units时间单位
Benchmark Mode Cnt Score Error Units
JMHTestHello01.dealHelloWorld avgt 5 1.009 ± 0.009 s/op
Process finished with exit code 0
通过上面的示例代码可以发现,JMH的使用并不复杂,代码量也并不多;很多的功能都是通过配置注解,或者生成Options的属性来进行配置的。因此我们要更好的使用JMH其他功能,就需要对他的一些基本配置要有所了解。
基准测试的模式。只有一个Mode属性。而这个Mode属性表示JMH度量的模式,或测试方式。
/**
* <p>Benchmark mode declares the default modes in which this benchmark
* would run. See {@link Mode} for available benchmark modes.</p>
*
* <p>This annotation may be put at {@link Benchmark} method to have effect
* on that method only, or at the enclosing class instance to have the effect
* over all {@link Benchmark} methods in the class. This annotation may be
* overridden with the runtime options.</p>
*/
@Inherited
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface BenchmarkMode {
/**
* @return Which benchmark modes to use.
* @see Mode
*/
Mode[] value();
}
Mode提供了多种方式:
Throughput("thrpt", "Throughput, ops/time")
AverageTime("avgt", "Average time, time/op")
SampleTime("sample", "Sampling time")
SingleShotTime("ss", "Single shot invocation time")
All("all", "All benchmark modes")
@Benchmark 类似于@Test,用于告诉JMH测试覆盖哪些方法。只能注解在方法上,有点类似在测试项目进行package时,JMH会针对注解了@Benchmark的方法生成Benchmark方法代码。通常情况下,每个Benchmark方法都运行在独立的进程中,互不干涉。
这个是配置类,对测试进行配置。通常需要指定一些参数,如执行测试类(include)、使用的进程个数(fork)、预热迭代次数(warmupInterations)等。在配置启动测试时执行,如上述代码:
Options options = new OptionsBuilder()
.include(JMHTestHello01.class.getSimpleName())
.warmupIterations(3) // 预热的次数, 3次
.warmupTime(TimeValue.seconds(2)) // 预热的时间,2s
.forks(1) // 测试的执行线程数量
.build();
迭代是JMH 的一次测量单位。在大部分测量模式下,一次迭代表示1秒。在这一秒内会不间断调用被测方法,并采样计算吞吐量、平均时间等。
可以使用OptionsBuilder来配置,也可以使用注解。
Options options = new OptionsBuilder()
.include(JMHTestHello01.class.getSimpleName())
.measurementIterations(3).build(); // 执行的次数, 3次
或
@Measurement(iterations = 3)
@Benchmark
public void dealHelloWorld() throws InterruptedException {
// 这里模拟该方法执行
Thread.sleep(1000);
}
由于 Java 虚拟机的 JIT 的存在,同一个方法在 JIT 编译前后的时间将会不同。通常只考虑方法在 JIT 编译后的性能。预热测试不会作为最终的统计结果,预热的目的是让Java虚拟机对被测试代码进行足够多的优化。
同样的,预热也可以通过OptionsBuilder来配置,也可以使用注解。
Options options = new OptionsBuilder()
.include(JMHTestHello01.class.getSimpleName())
.warmupIterations(3).build(); // 预热的次数, 3次
或
@Warmup(iterations = 3)
@Benchmark
public void dealHelloWorld() throws InterruptedException {
// 这里模拟该方法执行
Thread.sleep(1000);
}
通过 State 可以指定一个对象的作用范围,JMH中通过Scope来进行实例化和共享操作。
@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface State {
/**
* State scope.
* @return state scope
* @see Scope
*/
Scope value();
}
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.SECONDS)
@State(Scope.Benchmark)
public class JMHTestHello01 {
}
类似Junit测试类的代码生成工具,JMH也有相应的测试代码自动生成工具插件。
下载安装插件 JMH Java Microbenchmark Harness。
安装完成后,在需要i生成测试代码的地方鼠标右键 -> Generate -> Generate JMH Benchmark,就可以自动生成。
然后只需要按照实际需求更改需要测试的属性配置,就可以直接鼠标右键运行,查看结果了。
实际项目中,通过使用JMH,开发人员可以准确地测量和分析Java代码的性能,并进行性能调优和优化。它可以帮助开发人员更好地理解代码在不同环境下的性能表现,识别性能瓶颈,并找到优化的方向和策略。但是需要注意的是,JMH虽然功能强大,但在使用时需要谨慎选择测试场景和参数,并理解其使用的统计方法和度量指标,以确保测试结果的准确性和可靠性。