如何在java中做基准测试

news2024/11/27 2:41:44

最近公司在搞新项目,由于是实验性质,且不会直接面对客户的项目,这次的技术选型非常激进,如,直接使用了Java 17。

作为公司里练习两年半的个人练习生,我自然也是深度的参与到了技术选型的工作中。不知道大家在技术选型中有没有关注过技术组件给出的基准测试?比如说,HikariCP的基准测试:

又或者是Caffeine的基准测试:

如果你仔细阅读过它们的基准测试报告,你会发现一项很有意思的技术:Java Microbenchmark Harness,简称JMH。

Tips:有些技术只需要学会如何使用即可,没有必要非得“卷”源码;有些“小众”技术你没有听过,也不必慌,没有人是什么都会的。

认识JMH

接触JMH之前,我通常用System.currentTimeMillis()来计算方法的执行时间:

long start = System.currentTimeMillis();
......
long duration = System.currentTimeMillis() - start;

大部分时候这么做都很灵,但某些场景下JVM会进行JIT编译和内联优化,导致代码在优化前后的执行效率差别非常大,此时这个“土”方法就不灵了。那么该如何准确的计算方法的执行时间呢?

Java团队为开发者提供了JMH基准测试套件:

JMH is a Java harness for building, running, and analysing nano/micro/milli/macro benchmarks written in Java and other languages targeting the JVM.

JMH是用于构建,运行和分析Java和其它基于JVM的语言编写的程序的基准测试套件。JMH提供了预热的能力,通过预热让JVM知道哪些是热点代码,除此之外,JMH还提供了吞吐量的测试指标。相较于“土”方法,JMH可以支持更多种的测试场景,而且基于JMH得出的测试结果也会更全面,更准确

使用JMH

项目中引入JMH的依赖:

<dependency>
  <groupId>org.openjdk.jmh</groupId>
  <artifactId>jmh-core</artifactId>
  <version>1.36</version>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.openjdk.jmh</groupId>
  <artifactId>jmh-generator-annprocess</artifactId>
  <version>1.36</version>
</dependency>

引入依赖后就可以编写一个简单的基准测试了,这里使用简化后的JMH官方示例:

package org.openjdk.jmh.samples;

import org.openjdk.jmh.annotations.Benchmark;  
import org.openjdk.jmh.annotations.BenchmarkMode;  
import org.openjdk.jmh.annotations.Mode;  
import org.openjdk.jmh.annotations.OutputTimeUnit;  
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;  

import java.util.concurrent.TimeUnit;

public class JMHSample_02_BenchmarkModes {

	@Benchmark
	@BenchmarkMode(Mode.AverageTime)
	@OutputTimeUnit(TimeUnit.MILLISECONDS)
	public void measureAvgTime() throws InterruptedException {
		TimeUnit.MILLISECONDS.sleep(100);
	}

	public static void main(String[] args) throws RunnerException {
		Options opt = new OptionsBuilder()
		.include(JMHSample_02_BenchmarkModes.class.getSimpleName())
		.forks(1)
		.build();
		new Runner(opt).run();
	}
}

执行这个示例,会输出如下结果:

以空行为分割的话,JMH的输出可以分为3个部分:

  • 基础信息,包括环境信息和基准测试配置;
  • 测试信息,每次预热(Warmup)和正式执行(Iteration)的信息;
  • 结果信息,基准测试的结果。

Tips

  • IDEA中不能使用DeBug模式运行,否则会报错
  • 注意依赖中的scope标签为test,在src\main\java路径下是无法访问到JMH的。

启动测试

从示例中不难发现,在IDEA中执行测试需要先构建Options,并通过Runner去执行。我们来构建一个最简单的Options:

Options opt = new OptionsBuilder().build();

new Runner(opt).run();

这样的Options会执行散落在程序各处的基准测试方法(使用Benchmark注解的方法)。如果不需要执行所有的基准测试方法,通常在构建Options时会指定测试的范围:

Options opt = new OptionsBuilder().include(JMHSample_02_BenchmarkModes.class.getSimpleName()).build();

这时基准测试仅限于Test类中的基准测试方法。除此之外,你可能还会嫌弃控制台输出样式丑陋,或者要提交的基准测试报告中需要用图示来直观的表达,这个时候可以控制输出结果的格式并指定结果输出文件:

Options opt = new OptionsBuilder()
.include(JMHSample_02_BenchmarkModes.class.getSimpleName())
.result("result.json")
.resultFormat(ResultFormatType.JSON)
.build();

再结合以下网站,可以很轻松的构建出测试结果图示:

  • JMH Visual Chart (deepoove.com)
  • JMH Visualizer (morethan.io)

例如,我通过JMH Visual Chart构建出的测试结果:

实际上,OptionsBuilder提供的功能远不止如此,不过其中大部分功能都可以通过下文中提到注解进行配置,在此就不进行多余的说明了。

常用注解

JMH可以通过注解非常简单的完成基准测试的配置,接下来对其中常用的15个注解进行详细说明。

注解:Benchmark

注解Benchmark的声明:

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

Benchmark用于方法上且该方法必须使用public修饰,表明该方法为基准测试方法

注解:BenchmarkMode

注解BenchmarkMode的声明:

@Inherited  
@Target({ElementType.METHOD, ElementType.TYPE})  
@Retention(RetentionPolicy.RUNTIME)  
public @interface BenchmarkMode {
	Mode[] value();  
}

BenchmarkMode用于方法或类上,表明测试指标。枚举类Mode提供了4种测试指标:

  • Mode.Throughput,吞吐量,单位时间内执行的次数;
  • Mode.AverageTime,平均时间,执行方法的平均耗时;
  • Mode.SampleTime,操作时间采样,并输出结果分布;
  • Mode.SingleShotTime,单次操作时间,通常在不进行预热时测试冷启动的时间。

我们来看下Mode.SampleTime的输出结果:

除单独使用以上测试指标外,还可以指定Mode.All进行全部指标的基准测试。

注解:OutputTimeUnit

注解OutputTimeUnit的声明:

@Inherited
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface OutputTimeUnit { 
	TimeUnit value();
}

OutputTimeUnit用于方法或类上,表明输出结果的时间单位。好了,示例中的注解我们已经了解完毕,接下来我们看其它较为关键的注解。

注解:Timeout

注解Timeout的声明:

@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Timeout {

	int time();

	TimeUnit timeUnit() default TimeUnit.SECONDS;
}

Timeout用于方法或类上,指定了基准测试方法的超时时间

注解:Warmup

注解Warmup的声明:

@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Warmup {
	int BLANK_ITERATIONS = -1;
	int BLANK_TIME = -1;
	int BLANK_BATCHSIZE = -1;

	int iterations() default BLANK_ITERATIONS;

	int time() default BLANK_TIME;

	TimeUnit timeUnit() default TimeUnit.SECONDS;

	int batchSize() default BLANK_BATCHSIZE;
}

Warmup用于方法或类上,用于做预热配置。提供了4个参数:

  • iterations,预热迭代的次数;
  • time,每个预热迭代的时间;
  • timeUnit,时间单位;
  • batchSize,每个操作调用的次数。

预热的执行结果并不会被统计到测试结果中,因为JIT机制的存在某些方法被反复调用后,JVM会将其编译为机器码,使其执行效率大大提高。

注解:Measurement

注解Measurement的声明:

@Inherited
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Measurement {
	int BLANK_ITERATIONS = -1;
	int BLANK_TIME = -1;
	int BLANK_BATCHSIZE = -1;

	int iterations() default BLANK_ITERATIONS;

	int time() default BLANK_TIME;

	TimeUnit timeUnit() default TimeUnit.SECONDS;

	int batchSize() default BLANK_BATCHSIZE;
}

Measurement与Warmup的使用方法完全一致,参数含义也完全相同,区别在于Measurement属于正式测试的配置,结果会被统计

注解:Group

注解Group的声明:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Group {
	String value() default "group";
}

Group用于方法上,为测试方法分组

注解:State

注解State的声明:

@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface State {
	Scope value();
}

State用于类上,表明了类中变量的作用范围。枚举类Scope提供了3种作用域:

  • Scope.Benchmark,每个测试方法中使用一个变量;
  • Scope.Group,每个分组中使用同一个变量;
  • Scope.Thread,每个线程中使用同一个变量。

忘记了是在哪看到有人说Scope.Benchmark的作用域是所有的基准测试方法,这个是错误的,Scope.Benchmark会为每个基准测试方法生成一个对象,例如:

@State(Scope.Benchmark)
public static class ThreadState {
}

@Benchmark
@BenchmarkMode(Mode.SingleShotTime)
public void test1(State state) {
System.out.println("test1执行" + VM.current().addressOf(state));
}

@Benchmark
@BenchmarkMode(Mode.SingleShotTime)
public void test2(State state) {
System.out.println("test2执行" + VM.current().addressOf(state));
}

这个例子中,test1和test2使用的是不同的State对象。

Tips:VM.current().addressOf()是jol-core中提供的功能。

注解:Setup

注解Setup的声明:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Setup {
	Level value() default Level.Trial;
}

Setup用于方法上,基准测试前的初始化操作。枚举类Level提供了3个级别:

  • Level.Trial,所有基准测试执行时;
  • Level.Iteration,每次迭代时;
  • Level.Invocation,每次方法调用时。

Tips:一次迭代中,可能会出现多次方法调用。

注解:TearDown

注解TearDown的声明:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TearDown {
	Level value() default Level.Trial;
}

TearDown用于方法上,与Setup的作用相反,是基准测试后的操作,同样使用Level提供了3个级别。

注解:Param

注解Param的声明:

@Inherited
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Param {

	String BLANK_ARGS = "blank_blank_blank_2014";

	String[] value() default { BLANK_ARGS };
}

Param用于字段上,用于指定不同的参数,需要搭配State注解来使用。举个例子:

@State(Scope.Benchmark)
public class Test {
	@Param({"10", "100", "1000", "10000"})
	int count;

	@Benchmark
	@Warmup(iterations = 0)
	@BenchmarkMode(Mode.SingleShotTime)
	public void loop() throws InterruptedException {
		for(int i = 0; i < count; i++) {
			TimeUnit.MILLISECONDS.sleep(1);
		}
	}
}

上述代码测试了程序在循环10次,100次,1000次和10000次时的性能。

注解:Threads

注解Threads的声明:

@Inherited
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Threads {

	int MAX = -1;

	int value();
}

Threads用于方法和类上,指定基准测试中的并行线程数。当使用MAX时,将会使用所有可用线程进行测试,即Runtime.getRuntime().availableProcessors()返回的线程数。

注解:GroupThreads

注解GroupThreads的声明:

@Inherited
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface GroupThreads {
	int value() default 1;
}

GroupThreads用于方法上,指定基准测试分组中使用的线程数

注解:Fork

注解Fork的声明:

@Inherited  
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Fork {
	int BLANK_FORKS = -1;

	String BLANK_ARGS = "blank_blank_blank_2014";

	int value() default BLANK_FORKS;

	int warmups() default BLANK_FORKS;

	String jvm() default BLANK_ARGS;

	String[] jvmArgs() default { BLANK_ARGS };

	String[] jvmArgsPrepend() default { BLANK_ARGS };

	String[] jvmArgsAppend() default { BLANK_ARGS };
}

Fork用于方法和类上,指定基准测试中Fork的子进程。Fork提供了6个参数:

  • value,表示Fork出的子进程数量;
  • warmups,预热次数;
  • jvm,JVM的位置;
  • jvmArgs,需要替换的JVM参数;
  • jvmArgsPrepend,需要添加的JVM参数;
  • jvmArgsAppend,需要追加的JVM参数。

将Fork设置为0时,JMH会在当前JVM中运行基准测试。由于可能处于用户的JVM中,无法反应真实的服务端场景,无法准确的反应实际性能,因此JMH推荐进行Fork设置

另外可以利用Fork提供的JVM设置,将JVM设置为Server模式:

@Fork(value = 1, jvmArgsAppend = {"-Xmx1024m", "-server"})

注解:CompilerControl

注解CompilerControl的声明:

@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface CompilerControl {

	Mode value();

	enum Mode {
		BREAK("break"),
		PRINT("print"),
		EXCLUDE("exclude"),
		INLINE("inline"),
		DONT_INLINE("dontinline"),
		COMPILE_ONLY("compileonly");
	}
}

CompilerControl用于方法,构造器或类上,指定编译方式。其内部枚举类提供了6种编译方式:

  • BREAK,将断点插入到编译后的代码;
  • PRINT,打印方法及其配置;
  • EXCLUDE,禁止编译;
  • INLINE,使用内联;
  • DONT_INLINE,禁止内联;
  • COMPILE_ONLY,仅编译。

结语

关于JMH的使用,我们就聊到这里了,希望今天的内容能够帮助你学习并掌握一种更准确的性能测试方法。

最后提供一个练习使用JMH的思路:大家都看到了文章开头Caffeine给出的基准测试结果了,但由于是Caffeine作者自己提供的基准测试,难免有些“既当裁判又当选手”的嫌疑,或者说他选取了一些对Caffeine有利的角度来展示结果,那么可以结合你自己的实际使用场景,给Caffeine及其竞品做一次基准测试。


如果本文对你有帮助的话,还请多多点赞支持。如果文章中出现任何错误,还请批评指正。最后欢迎大家关注分享硬核Java技术的金融摸鱼侠王有志,我们下次再见!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/969199.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Gitea--私有git服务器搭建详细教程

一.官方文档 https://docs.gitea.com/zh-cn/说明 gitea 是一个自己托管的Git服务程序。他和GitHub, Gitlab等比较类似。他是从 Gogs 发展而来&#xff0c;gitea的创作团队重新fork了代码&#xff0c;并命名为giteagitea 功能特性多&#xff0c;能够满足我们所有的的代码管理需…

预推免,保研------长安大学保内,附加分面试准备【记录帖】

&#x1f680;长安大学——人工智能系——程惠泽 &#x1f68c;前六学期专业排名&#xff1a;7/82 &#x1f68c;信息门户GPA&#xff1a;3.94 &#x1f68c;平均成绩&#xff1a;89.83 &#x1f68c;加权成绩&#xff1a;89.15 / ☁️本人比较菜&#xff0c;只能保研本校&…

认识doubbo和rpc

开个新坑&#xff0c;和大家一起学习Dubbo 3.X。我们按照一个由浅入深顺序来学习&#xff0c;先从使用Dubbo开始&#xff0c;再深入Dubbo的核心原理。 今天我们就从认识Dubbo开始&#xff0c;整体的内容可以分为3个部分&#xff1a; Dubbo是什么RPC是什么Dubbo的架构 正式开…

面试如何回答弹性盒子布局这个问题呢?

在我们面试中如果被问道css方面的面试题 那么极有可能被问到的一道面试题就是弹性盒子&#xff0c;本篇文章通过一张图带你拿捏这道面试题。 1、首先需要说一说弹性盒子的基本概念&#xff1a;弹性盒子是一种用于网页布局中创建灵活和响应式设计的CSS布局模型。 2、其次需要说…

父组件调用子组件 ref 不生效?组件暴露 ref ?

向你的组件暴露 ref 要暴露 ref 最关键的就是 forwardRef forwardRef 是 React 中的一个高阶函数&#xff0c;用于在函数组件中将 ref 属性向下传递给子组件。 在 React 中&#xff0c;我们可以使用 ref 属性来获取对一个组件实例的引用&#xff0c;以便在父组件中操作子组件。…

IDEA复制一个工程为多个并启动,测试负载均衡

1 找到服务按钮 2 选择复制配置 3 更改新的名称与虚拟机参数 复制下面的代码在VM参数中 -Dserver.port8082 4 最后启动即可

【80天学习完《深入理解计算机系统》】第十二天3.6数组和结构体

专注 效率 记忆 预习 笔记 复习 做题 欢迎观看我的博客&#xff0c;如有问题交流&#xff0c;欢迎评论区留言&#xff0c;一定尽快回复&#xff01;&#xff08;大家可以去看我的专栏&#xff0c;是所有文章的目录&#xff09;   文章字体风格&#xff1a; 红色文字表示&#…

排序之归并排序

文章目录 前言一、归并排序1、归并排序基本思想2、归并排序代码实现3、归并排序效率分析 二、归并排序非递归实现(循环实现)1、归并排序非递归实现(循环实现)基本思想2、归并排序非递归实现(循环实现)代码 三、计数排序1、计数排序基本思想2、计数排序代码实现3、计数排序效率分…

大数据-玩转数据-Flink状态编程(上)

一、Flink状态编程 有状态的计算是流处理框架要实现的重要功能&#xff0c;因为稍复杂的流处理场景都需要记录状态&#xff0c;然后在新流入数据的基础上不断更新状态。 SparkStreaming在状态管理这块做的不好, 很多时候需要借助于外部存储(例如Redis)来手动管理状态, 增加了编…

顺式元件热图+柱状图

写在前面 本教程来自粉丝投稿&#xff0c;主要做得是顺式元件的预测和热图绘制。类似的教程&#xff0c;在我们基于TBtools做基因家族分析也做过&#xff0c;流程基本一致。我们前期的教程&#xff0c;主要是基于TBtools&#xff0c;本教程主要是基于纯代码,也是值得学习收藏的…

【人工智能】—_线性分类器、感知机、损失函数的选取、最小二乘法分类、模型复杂性和过度拟合、规范化

文章目录 Linear predictions 线性预测分类线性分类器感知机感知机学习策略损失函数的选取距离的计算 最小二乘法分类求解最小二乘分类矩阵解法一般线性分类模型复杂性和过度拟合训练误差测试误差泛化误差复杂度与过拟合规范化 Linear predictions 线性预测 分类 从具有有限离…

2022年03月 C/C++(七级)真题解析#中国电子学会#全国青少年软件编程等级考试

C/C编程&#xff08;1~8级&#xff09;全部真题・点这里 第1题&#xff1a;红与黑 有一间长方形的房子&#xff0c; 地上铺了红色、 黑色两种颜色的正方形瓷砖。你站在其中一块黑色的瓷砖上&#xff0c; 只能向相邻的黑色瓷砖移动。 请写一个程序&#xff0c; 计算你总共能够到…

Aqs的CyclicBarrier。

今天我们来学习AQS家族的“外门弟子”&#xff1a;CyclicBarrier。 为什么说CyclicBarrier是AQS家族的“外门弟子”呢&#xff1f;那是因为CyclicBarrier自身和内部类Generation并没有继承AQS&#xff0c;但在源码的实现中却深度依赖AQS家族的成员ReentrantLock。就像修仙小说…

Java 复习笔记 - 学生管理系统篇

文章目录 学生管理系统一&#xff0c;需求部分需求分析初始菜单学生类添加功能删除功能修改功能查询功能 二&#xff0c;实现部分&#xff08;一&#xff09;初始化主界面&#xff08;二&#xff09;编写学生类&#xff08;三&#xff09;编写添加学生方法&#xff08;四&#…

ref 操作 React 定时器

秒表 需要将 interval ID 保存在 ref 中&#xff0c;以便在需要时能够清除计时器。 import { useRef, useState } from "react";const SecondWatch () > {const [startTime, setStartTime] useState<any>(null);const [now, setNow] useState<any>…

Elasticsearch中RestClient使用

&#x1f353; 简介&#xff1a;java系列技术分享(&#x1f449;持续更新中…&#x1f525;) &#x1f353; 初衷:一起学习、一起进步、坚持不懈 &#x1f353; 如果文章内容有误与您的想法不一致,欢迎大家在评论区指正&#x1f64f; &#x1f353; 希望这篇文章对你有所帮助,欢…

如何将自己的镜像使用 helm 部署

本文分别从如下几个方面来分享一波 如何将自己的镜像使用 helm 部署 简单介绍一下 helm 使用自己写 yaml 文件的方式在 k8s 中部署应用 使用 helm 的方式在 k8s 中部署应用 简单介绍一下 helm Helm 是 Kubernetes 的包管理器&#xff0c;在云原生领域用于应用打包和分发 Hel…

12. 微积分 - 梯度积分

Hi,大家好。我是茶桁。 上一节课,我们讲了方向导数,并且在最后留了个小尾巴,是什么呢?就是梯度。 我们再来回看一下但是的这个式子: [ f x f y

信息系统项目管理师(第四版)教材精读思维导图-第八章项目整合管理

请参阅我的另一篇文章&#xff0c;综合介绍软考高项&#xff1a; 信息系统项目管理师&#xff08;软考高项&#xff09;备考总结_计算机技术与软件专业技术_铭记北宸的博客-CSDN博客 本章思维导图PDF格式 本章思维导图XMind源文件 目录 8.1 管理基础 8.2 管理过程 8.3 制定项…

LRU算法 vs Redis近似LRU算法

LRU(Least Recently Use)算法&#xff0c;是用来判断一批数据中&#xff0c;最近最少使用算法。它底层数据结构由Hash和链表结合实现&#xff0c;使用Hash是为了保障查询效率为O(1)&#xff0c;使用链表保障删除元素效率为O(1)。 LRU算法是用来判断最近最少使用到元素&#xf…