JMH - 微基准测试

本文展示了如何利用Spring框架中的org.springframework.util.CollectionUtils.isEmpty()方法来判断List集合是否为空。通过三个示例,分别演示了空List、null List以及包含元素的List的情况,说明该方法能正确判断集合的空状态。
摘要由CSDN通过智能技术生成

一、介绍

JMH全称Java Microbenchmark Harness(微基准测试框架),是专门用于Java代码微基准测试的一套测试工具API,是由OpenJDK/Oracle官方发布的工具。

二、快速使用

在pom.xml文件中引入下述依赖:

<dependency>
	<groupId>org.openjdk.jmh</groupId>
	<artifactId>jmh-core</artifactId>
	<version>1.33</version>
</dependency>

<dependency>
	<groupId>org.openjdk.jmh</groupId>
	<artifactId>jmh-generator-annprocess</artifactId>
	<version>1.33</version>
	<scope>provided</scope>
</dependency>

构建基准测试的“HelloWorld”示例,用于测试String与StringBuffer拼接字符串的吞吐量: 

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.results.format.ResultFormatType;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;

@BenchmarkMode({Mode.Throughput})
@OutputTimeUnit(TimeUnit.SECONDS)
@Fork(value = 1)
@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS)
@Threads(1)
public class HelloWorld {

    @Benchmark
    public void measureBuffer() {
        String[] helloWorld = {"h", "e", "l", "l", "o", "W", "o", "r", "l", "d"};
        StringBuffer stringBuffer = new StringBuffer();
        for (int i = 0; i < helloWorld.length; i++) {
            stringBuffer.append(helloWorld[i]);
        }
    }

    @Benchmark
    public void measureGeneral() {
        String[] helloWorld = {"h", "e", "l", "l", "o", "W", "o", "r", "l", "d"};
        String data = "";
        for (int i = 0; i < helloWorld.length; i++) {
            data = data + helloWorld[i];
        }
    }

    public static void main(String[] args) throws Exception {
        Options opts = new OptionsBuilder()
                .include(HelloWorld.class.getSimpleName())
                .resultFormat(ResultFormatType.CSV)
                .build();
        new Runner(opts).run();
    }
}

执行上述入门示例的main方法,其输出结果如下: 

可以看到StringBuffer拼接字符串的吞吐量要远比String拼接字符串的吞吐量大很多,因此在使用上关于字符串拼接优先使用StringBuffer,并且它是线程安全的。 

三、注解介绍

3.1 @BenchmarkMode

该注解一般作用在公共类上,其取值是一个列表,其配置示例为@BenchmarkMode({Mode.Throughput, Mode.AverageTime}),共有下述五种取值:

Mode.Throughput:表示测试的是吞吐量。

Mode.AverageTime:表示测试的是平均时间,即每次调用平均耗时。

Mode.SampleTime:表示测试的是随机取样(具体这个的作用是什么,目前我还不知道)

Mode.SingleShotTime:顾名思义,只运行一次,一般用于测试冷启动性能。

Mode.All:表示的是将前面这四种类型全部进行测试

3.2 @OutputTimeUnit 

该注解表示测试结果的时间单位,其配置示例为@OutputTimeUnit(TimeUnit.SECONDS)

  • TimeUnit.MILLISECONDS: 表示毫秒
  • TimeUnit.SECONDS:表示秒

3.3 @Benchmark

该注解用于标识需要进行基准测试的方法,类比JUnit的@Test。这个注解告诉JMH,‌被注解的方法是需要进行性能测试的目标。‌通过使用@Benchmark注解,‌开发者可以指定哪些方法需要进行性能测试,‌从而帮助评估代码的性能并进行优化。‌

3.4 @Fork

如果@Fork注解的value值为3的话,则JMH会fork出3组进程来进行测试。

3.5 @Warmup

该注解表示“预热”阶段的配置,注解中iterations属性的值为1则表示在每一组进程中会进行1轮的预热。注解中time属性的值为2,timeUnit属性的值为TimeUnit.SECONDS,则表示每一轮预热处理的总时间为2秒。

3.6 @Measurement

该注解表示“压测”阶段的配置,注解中iterations属性的值为1则表示在每一组进程中会进行1轮的压测。注解中time属性的值为2,timeUnit属性的值为TimeUnit.SECONDS,则表示每一轮压测的总时间为2秒。

3.7 @Threads

该注解表示在每一轮“预热”和“压测”处理中的线程数设置有多少个。

3.8 @State

该标签用于指定一个属性共享的范围(了解这个标签前,先看完下面的文章,需要对基准测试有一个大概的认识) 

3.8.1 基准测试内线程内共享

在JMH类中定义如下这个属性(静态内部类):

@State(Scope.Thread)
public static class ThreadState {
    volatile double k = 1;
}

然后再对应的Benchmark方法当中使用:

@Benchmark
public void measureUnshared(ThreadState state) throws InterruptedException {
    state.k++;
    Thread.sleep(500);
    System.out.println("thread name = " + Thread.currentThread().getName() + "  ,state= " + state.hashCode() + "  ,k " +
            "= " + state.k);
}

基准方法可以引用状态,并且JMH将在调用这些方法时注入适当的状态。  在上面的基准方法当中就包含了一个状态,在这个方法运行时,JMH会自动注入这个对象。

开启4个线程来测试,对应的主类方法如下:

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.results.format.ResultFormatType;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;

@BenchmarkMode({Mode.Throughput})
@OutputTimeUnit(TimeUnit.SECONDS)
@Fork(value = 1)
@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS)
@Threads(4)
public class HelloWorld {

    @State(Scope.Thread)
    public static class ThreadState {
        volatile double k = 1;
    }

    @Benchmark
    public void measureUnshared(ThreadState state) throws InterruptedException {
        state.k++;
        Thread.sleep(500);
        System.out.println("thread name = " + Thread.currentThread().getName() + "  ,state= " + state.hashCode() + "  ,k " +
                "= " + state.k);
    }

    public static void main(String[] args) throws Exception {
        Options opts = new OptionsBuilder()
                .include(HelloWorld.class.getSimpleName())
                .resultFormat(ResultFormatType.CSV)
                .build();
        new Runner(opts).run();
    }
}

以下为第一轮预热的结果:

从这个结果可以看出,JMH使用了四个线程,这个四个线程使用的state对象都是不同的(从hashCode不同可以看),每个都有自己的独一份,也就是说通过 @State(Scope.Thread) 将这个对象限定为线程内部共享了。在上面同一个线程中,下一轮测试会使用上面的值,比如以下为预热完成之后,真正开始测试的第一轮结果。

从这个结果可以看出,Measurement阶段与Warmup阶段使用的线程相同,线程内的对象也是同一个对象,对象内的变量被共享(k的值持续增加)。

3.8.2 基准测试内线程间共享

如果想一个变量在同一个基础测试内被所有线程共享,可定义对象如下:

@State(Scope.Benchmark)
public static class ThreadState {
    volatile double k = 1;
}
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.results.format.ResultFormatType;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;

@BenchmarkMode({Mode.Throughput})
@OutputTimeUnit(TimeUnit.SECONDS)
@Fork(value = 1)
@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS)
@Threads(4)
public class HelloWorld {

@State(Scope.Benchmark)
public static class ThreadState {
    volatile double k = 1;
}

    @Benchmark
    public void measureShared(ThreadState state) throws InterruptedException {
        state.k++;
        Thread.sleep(1000);
        System.out.println("thread name = " + Thread.currentThread().getName() + "  ,state= " + state.hashCode() + "  ,k " +
                "= " + state.k);
    }

    public static void main(String[] args) throws Exception {
        Options opts = new OptionsBuilder()
                .include(HelloWorld.class.getSimpleName())
                .resultFormat(ResultFormatType.CSV)
                .build();
        new Runner(opts).run();
    }
}

 从这里可以看出两个结论:第一个就是不同的线程之间确实是共享了同一个对象,但是共享对象中的变量值看起来好像不对(总量是对的,但是单次取的值不对)。目前这个小问题,不是重点,重点是了解其相关注解的作用。

四、吞吐量测试(推荐使用)

Mode.AverageTime 表示测量吞吐量,即单位时间内可以运行多少次。

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.results.format.ResultFormatType;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;

@BenchmarkMode({Mode.Throughput})
@OutputTimeUnit(TimeUnit.SECONDS)
@Fork(value = 1)
@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS)
@Threads(1)
public class HelloWorld {

    @Benchmark
    public void measureThroughput() {
        String[] helloWorld = {"h", "e", "l", "l", "o", "W", "o", "r", "l", "d"};
        String data = "";
        for (int j = 0; j < helloWorld.length; j++) {
            data = data + helloWorld[j];
        }
    }

    public static void main(String[] args) throws Exception {
        Options opts = new OptionsBuilder()
                .include(HelloWorld.class.getSimpleName())
                .resultFormat(ResultFormatType.CSV)
                .build();
        new Runner(opts).run();
    }
}

运行的时候会在控制台打印结果,下面对测试结果做简要分析。

以下为预热的吞吐量和测试的吞吐量,其中的ops/s表示每秒操作次数: 

五、平均耗时测试

Mode.AverageTime 表示测量平均执行时间,它的执行方式类似于 Mode.Throughput。有人可能会说这是吞吐量的倒数,的确如此,在某些工作负载中,测量时间更方便。 

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.results.format.ResultFormatType;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;

@BenchmarkMode({Mode.AverageTime})
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Fork(value = 1)
@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS)
@Threads(1)
public class HelloWorld {

    @Benchmark
    public void measureAvgTime() {
        String[] helloWorld = {"h", "e", "l", "l", "o", "W", "o", "r", "l", "d"};
        String data = "";
        for (int i = 1; i <= 5000; i++) {
            for (int j = 0; j < helloWorld.length; j++) {
                data = data + helloWorld[j];
            }
        }
    }

    public static void main(String[] args) throws Exception {
        Options opts = new OptionsBuilder()
                .include(HelloWorld.class.getSimpleName())
                .resultFormat(ResultFormatType.CSV)
                .build();
        new Runner(opts).run();
    }
}

执行上述代码,在输出结果中可以看到其平均耗时在200ms左右: 

特别说明:JMH基准测试的时间和实际应用程序中日志打印的时间不一样,主要是因为JMH专注于测量代码本身的执行时间,而忽略了日志打印等外部开销,同时还会受到CPU调度和外部系统响应时间的影响‌。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值