Java性能测试Benchmark使用总结

如何测量Java代码的性能

在 Java 中,可以使用多种方法来测量一段代码的执行性能。使用 System.currentTimeMillis()是最常见的方法

long startTime = System.currentTimeMillis();

// 需要测量的代码块
for (int i = 0; i < 1000000; i++) {
    // 示例代码
}

long endTime = System.currentTimeMillis();
long duration = endTime - startTime; // 执行时间(毫秒)
System.out.println("Execution time: " + duration + " ms");

但是这么测结果是不太准确的,因为Java存在代码优化以及JIT编译等等,通常更准确的方式是使用Benchmark的方式来评估性能。

JMH基本使用

参考:https://github.com/openjdk/jmh
Java Microbenchmark Harness (JMH) 是一个用于基准测试 Java 代码的工具。它能够准确测量代码的性能,帮助开发者了解不同实现的效率。

首先添加依赖,jmh一般用于test,所以scope可以指定为test

<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-core</artifactId>
    <version>1.35</version>
</dependency>
<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-generator-annprocess</artifactId>
    <version>1.35</version>
</dependency>

示例如下:

package org.example;

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

import java.util.Arrays;
import java.util.concurrent.TimeUnit;

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
public class JMHExample {

    private int[] numbers;

    public static void main(String[] args) throws Exception {
        Options opts = new OptionsBuilder()
                .include(JMHExample.class.getSimpleName()) // 指定测试的类
                .build();
        new Runner(opts).run();  // 运行
    }

    @Setup(Level.Trial)
    public void setup() {
        numbers = new int[1000];
        for (int i = 0; i < numbers.length; i++) {
            numbers[i] = i;
        }
    }

    @Benchmark
    public int sumArray() {
        int sum = 0;
        for (int number : numbers) {
            sum += number;
        }
        return sum;
    }

    @Benchmark
    public int sumArrayParallel() {
        return Arrays.stream(numbers).parallel().sum();
    }
}

在示例中,其实我们的目的就是比较两种对数组求和方式的性能,看看每秒的吞吐量如何,运行测试,可以在控制台得到相应输出。

常用注解

@Benchmark

@Benchmark 注解是用于标记测试方法的,类似 JUnit 中的 @Test 注解需要单元测试的方法一样,只有被这个注解标记的方法才会参与基准测试,且被标记的方法必须是 public 的。在一个基本测试类中至少包含一个被 @Benchmark 标记的方法,否则会抛出异常。

@BenchmarkMode

@BenchmarkMode 注解用于指定基准测试的模式。JMH 共有四种模式:

  1. Throughput:整体吞吐量,例如“1 秒内可以执行多少次调用”。
  2. AverageTime:调用的平均时间,例如“每次调用平均耗时 xxx 毫秒”。如果需要测试某个方法的平均耗时,可以使用@BenchmarkMode 注解并指定基准测试的模式为 AverageTime。
  3. SampleTime:随机取样,最后输出取样结果的分布,例如“99%的调用在 xxx 毫秒以内,99.99%的调用在 xxx 毫秒以内”。
  4. SingleShotTime:以上模式都是默认一次 iteration 是 1s,唯有 SingleShotTime 是只运行一次。往往同时把 warmup 次数设为 0,用于测试冷启动时的性能。
  5. All:所有的指标全算一遍

在使用时,@BenchmarkMode 注解可设置在类上也可以设置在基准方法上。例如:

@Benchmark
@BenchmarkMode(Mode.AverageTime)
public void methodToTest() {
    // 测试代码
}

@OutputTimeUnit

基准测试结果的时间类型。一般选择秒、毫秒、微秒,这里填入的是 TimeUnit 这个枚举类型,涉及单位很多从纳秒到天都有,按需选择,最终输出易读的结果。

@State

@State 指定了在类中变量的作用范围。@State 用于声明某个类是一个“状态”,可以用Scope 参数用来表示该状态的共享范围。这个注解必须加在类上,否则提示无法运行。它有三个取值。

  1. Benchmark:表示变量的作用范围是某个基准测试类。
  2. Thread:每个线程一份副本,如果配置了Threads注解,则每个Thread都拥有一份变量,它们互不影响。
  3. Group:联系上面的@Group注解,在同一个Group里,将会共享同一个变量实例。

本例中,相关变量的作用范围是 Benchmark。

@Warmup

预热,可以加在类上或者方法上,预热只是测试数据,是不作为测量结果的。

该注解一共有4个参数:

  1. iterations 预热阶段的迭代数
  2. time 每次预热时间
  3. timeUnit 时间单位,通常秒
  4. batchSize 批处理大小,指定每次操作调用几次方法

本例中,我们加在类上,让它迭代3次,每次1秒,时间单位秒。

@Setup

注解的作用就是我们需要在测试之前进行一些准备工作,比如对一些数据的初始化之类的,这个也和Junit的@Before等类似

@Teardown

在测试之后进行一些结束工作,主要用于资源回收

@Measurement

和预热类似,这个注解是会影响测试结果的,它的参数和 Warmup 一样,这里不多介绍。
本例中我们在迭代中设置的是5次,每次1秒。
通常 @Warmup 和 @Measurement 两个参数会一起使用。

@Fork

表示开启几个进程测试,通常我们设为1,如果数值大于1,则启用新的进程测试,如果设置为0,程序依然进行,但是在用户的 JVM 进程上运行。

@Threads

上面的注解注重开启几个进程,这里就是开启几个线程,只有一个参数 value,指定注解的value,将会开启并行测试,如果设置的 value 过大,如 Threads.Max,则使用处理机的相同线程数。

输出格式

public static void main(String[] args) throws RunnerException {
    Options opts = new OptionsBuilder()
            // 表示包含的测试类
            .include(JMHExample.class.getSimpleName()) 
            // 最后结果输出文件的命名,不指定默认为jmh-reuslt.json
            .result("benchmark.json")
            // 结果输出什么格式,可以是json, csv, text等
            .resultFormat(ResultFormatType.JSON)
            .build();
 
    new Runner(opts).run(); // 运行
}

作为程序开发人员,看懂测试结果没难度,测试结果文本能可视化更好。好在我们拿到了JMH 结果后,根据文件格式,我们可以二次加工,就可以图表化展示[2]。

JMH 支持的几种输出格式:

  1. TEXT 导出文本文件。
  2. CSV 导出csv格式文件。
  3. SCSV 导出scsv等格式的文件。
  4. JSON 导出成json文件。
  5. LATEX 导出到latex,一种基于ΤΕΧ的排版系统。

比如 CSV 格式的文件,我们就可以通过 EXCEL 处理获取图表,当然也还有其他的一些工具,例如:

  1. https://jmh.morethan.io/,参考:https://github.com/jzillmann/jmh-visualizer
    在这里插入图片描述
    这个网站要使用json格式的输出结果
    在这里插入图片描述

jmh-generator

JMH生成测试代码的方式有多种,例如jmh-generator-annprocessjmh-generator-reflection 是 JMH(Java Microbenchmark Harness)中用于基准测试生成的两个核心组件。

  1. jmh-generator-annprocess:这个模块用于在编译时处理基准测试的注解。它通过注解处理器生成相应的基准测试代码。
  • 性能:由于是在编译时生成代码,运行时的开销较小,因此性能较好。

  • 类型安全:在编译时生成的代码是类型安全的,可以捕获潜在的错误。

使用场景:适合于需要大量基准测试并且希望在编译时进行优化的场景。

  1. jmh-generator-reflection:这个模块使用反射机制在运行时生成基准测试代码,在运行时读取类的结构,并生成相应的基准测试实现。
  • 优点

    • 灵活性:可以动态生成基准测试,适合那些在编译时无法确定的基准测试场景。
    • 简化:对于简单或快速的基准测试,开发者无需进行额外的编译配置。
  • 缺点

    • 性能开销:由于使用反射,运行时性能开销相对较大。
    • 类型安全:可能导致运行时错误,缺乏编译时检查。

如果需要高性能、类型安全的基准测试,并且可以接受编译时的复杂性。选择 jmh-generator-annprocess,如果希望动态生成基准测试,或者在快速原型开发时需要更灵活的解决方案。选择 jmh-generator-reflection

对应的maven依赖如下

参考:https://mvnrepository.com/artifact/org.openjdk.jmh

<!-- 基于注解处理器生成 -->
<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-generator-annprocess</artifactId>
    <version>1.37</version>
    <scope>test</scope>
</dependency>
<!-- 基于反射生成 -->
<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-generator-reflection</artifactId>
    <version>1.37</version>
    <scope>test</scope>
</dependency>
<!-- 基于字节码生成 -->
<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-generator-bytecode</artifactId>
    <version>1.37</version>
    <scope>test</scope>
</dependency>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值