软件测试最新顶级Java才懂的,基准测试JMH(1),2024年最新【干货】

img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上软件测试知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

需要这份系统化的资料的朋友,可以戳这里获取

样例如下。

@Measurement(

iterations = 5,

time = 1,

timeUnit = TimeUnit.SECONDS)

复制代码

MeasurementWarmup的参数是一样的。不同于预热,它指的是真正的迭代次数。

我们能够从日志中看到这个执行过程:

Measurement: 5 iterations, 1 s each

Iteration 1: 1646.000 ns/op

Iteration 2: 1243.000 ns/op

Iteration 3: 1273.000 ns/op

Iteration 4: 1395.000 ns/op

Iteration 5: 1423.000 ns/op

复制代码

虽然经过预热之后,代码都能表现出它的最优状态,但一般和实际应用场景还是有些出入的。如果你的测试机器性能很高,或者你的测试机资源利用已经达到了极限,都会影响测试结果的数值。通常情况下,我都会在测试的时候,给机器充足的资源,保持一个稳定的环境。在分析结果的时候,也更加关注不同实现方式的性能差异,而不是测试数据本身。

@BenchmarkMode

此注解用来指定基准测试类型,对应Mode选项,用来修饰类和方法都可以。这里的value,是一个数组,可以配置多个统计维度。比如:

@BenchmarkMode({Throughput,Mode.AverageTime})。统计的就是吞吐量和平均执行时间两个指标。

所谓的模式,在JMH中,可以分为以下几种:

  • Throughput: 整体吞吐量,比如QPS,单位时间内的调用量等。

  • AverageTime: 平均耗时,指的是每次执行的平均时间。如果这个值很小不好辨认,可以把统计的单位时间调小一点。

  • SampleTime: 随机取样

  • SingleShotTime: 如果你想要测试仅仅一次的性能,比如第一次初始化花了多长时间,就可以使用这个参数,其实和传统的main方法没有什么区别。

  • All: 所有的指标,都算一遍,你可以设置成这个参数看下效果。

我们拿平均时间,看一下一个大体的执行结果:

Result “com.github.xjjdog.tuning.BenchmarkTest.shift”:

2.068 ±(99.9%) 0.038 ns/op [Average]

(min, avg, max) = (2.059, 2.068, 2.083), stdev = 0.010

CI (99.9%): [2.030, 2.106] (assumes normal distribution)

复制代码

由于我们声明的时间单位是纳秒,本次shift方法的平均响应时间就是2.068纳秒。

我们也可以看下最终的耗时时间。

Benchmark Mode Cnt Score Error Units

BenchmarkTest.div avgt 5 2.072 ± 0.053 ns/op

BenchmarkTest.shift avgt 5 2.068 ± 0.038 ns/op

复制代码

由于是平均数,这里的Error值的是误差的意思(或者波动)。

可以看到,在衡量这些指标的时候,都有一个时间维度,它就是通过**@OutputTimeUnit**注解进行配置的。

这个就比较简单了,它指明了基准测试结果的时间类型。可用于类或者方法上。一般选择秒、毫秒、微秒,纳秒那是针对的速度非常快的方法。

举个例子,@BenchmarkMode(Mode.Throughput)@OutputTimeUnit(TimeUnit.MILLISECONDS)进行组合,代表的就是每毫秒的吞吐量。

如下面的关于吞吐量的结果,就是以毫秒计算的。

Benchmark Mode Cnt Score Error Units

BenchmarkTest.div thrpt 5 482999.685 ± 6415.832 ops/ms

BenchmarkTest.shift thrpt 5 480599.263 ± 20752.609 ops/ms

复制代码

OutputTimeUnit注解同样可以修饰类或者方法,通过更改时间级别,可以获取更加易读的结果。

@Fork

fork的值一般设置成1,表示只使用一个进程进行测试;如果这个数字大于1,表示会启用新的进程进行测试;但如果设置成0,程序依然会运行,不过这样是在用户的JVM进程上运行的,可以看下下面的提示,但不推荐这么做。

Fork: N/A, test runs in the host VM

*** WARNING: Non-forked runs may silently omit JVM options, mess up profilers, disable compiler hints, etc. ***

*** WARNING: Use non-forked runs only for debugging purposes, not for actual performance runs. ***

复制代码

那么fork到底是在进程还是线程环境里运行呢?我们追踪一下JMH的源码,发现每个fork进程是单独运行在Proccess进程里的,这样就可以做完全的环境隔离,避免交叉影响。它的输入输出流,通过Socket连接的模式,发送到我们的执行终端。

image.png

在这里分享一个小技巧。其实fork注解有一个参数叫做jvmArgsAppend,我们可以通过它传递一些JVM的参数。

@Fork(value = 3, jvmArgsAppend = {“-Xmx2048m”, “-server”, “-XX:+AggressiveOpts”})

复制代码

在平常的测试中,也可以适当增加fork数,来减少测试的误差。

@Threads

fork是面向进程的,而Threads是面向线程的。指定了这个注解以后,将会开启并行测试。

如果配置了 Threads.MAX ,则使用和处理机器核数相同的线程数。

@Group

@Group注解只能加在方法上,用来把测试方法进行归类。如果你单个测试文件中方法比较多,或者需要将其归类,则可以使用这个注解。

与之关联的@GroupThreads注解,会在这个归类的基础上,再进行一些线程方面的设置。

@State

@State 指定了在类中变量的作用范围。它有三个取值。

@State 用于声明某个类是一个“状态”,可以用Scope 参数用来表示该状态的共享范围。这个注解必须加在类上,否则提示无法运行。

Scope有如下3种值:

  • Benchmark:表示变量的作用范围是某个基准测试类。

  • Thread:每个线程一份副本,如果配置了Threads注解,则每个Thread都拥有一份变量,它们互不影响。

  • Group:联系上面的@Group注解,在同一个Group里,将会共享同一个变量实例。

JMHSample04DefaultState测试文件中,演示了变量x的默认作用范围是Thread,关键代码如下:

@State(Scope.Thread)

public class JMHSample_04_DefaultState {

double x = Math.PI;

@Benchmark

public void measure() {

x++;

}

}

复制代码

@Setup和@TearDown

和单元测试框架JUnit类似,用于基准测试前的初始化动作, @TearDown 用于基准测试后的动作,来做一些全局的配置。

这两个注解,同样有一个Level值,标明了方法运行的时机,它有三个取值。

  • Trial:默认的级别。也就是Benchmark级别。

  • Iteration:每次迭代都会运行。

  • Invocation:每次方法调用都会运行,这个是粒度最细的。

@Param

@Param 注解只能修饰字段,用来测试不同的参数,对程序性能的影响。配合@State注解,可以同时制定这些参数的执行范围。

代码样例如下:

@BenchmarkMode(Mode.AverageTime)

@OutputTimeUnit(TimeUnit.NANOSECONDS)

@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)

@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)

@Fork(1)

@State(Scope.Benchmark)

public class JMHSample_27_Params {

@Param({“1”, “31”, “65”, “101”, “103”})

public int arg;

@Param({“0”, “1”, “2”, “4”, “8”, “16”, “32”})

public int certainty;

@Benchmark

public boolean bench() {

return BigInteger.valueOf(arg).isProbablePrime(certainty);

}

public static void main(String[] args) throws RunnerException {

Options opt = new OptionsBuilder()

.include(JMHSample_27_Params.class.getSimpleName())

// .param(“arg”, “41”, “42”) // Use this to selectively constrain/override parameters

.build();

new Runner(opt).run();

}

}

复制代码

值得注意的是,如果你设置了非常多的参数,这些参数将执行多次,通常会运行很长时间。比如参数1 M个,参数2 N个,那么总共要执行M*N次。

下面是一个执行结果的截图。

image.png

@CompilerControl

这可以说是一个非常有用的功能了。

Java中方法调用的开销是比较大的,尤其是在调用量非常大的情况下。拿简单的getter/setter方法来说,这种方法在Java代码中大量存在。我们在访问的时候,就需要创建相应的栈帧,访问到需要的字段后,再弹出栈帧,恢复原程序的执行。

如果能够把这些对象的访问和操作,纳入到目标方法的调用范围之内,就少了一次方法调用,速度就能得到提升,这就是方法内联的概念。如图所示,代码经过JIT编译之后,效率会有大的提升。

image.png

这个注解可以用在类或者方法上,能够控制方法的编译行为,常用的有3种模式。

强制使用内联(INLINE),禁止使用内联(DONT_INLINE),甚至是禁止方法编译(EXCLUDE)等。

2.将结果图形化


使用JMH测试的结果,可以二次加工,进行图形化展示。结合图表数据,更加直观。通过运行时,指定输出的格式文件,即可获得相应格式的性能测试结果。

比如下面这行代码,就是指定输出JSON格式的数据。

Options opt = new OptionsBuilder()

.resultFormat(ResultFormatType.JSON)

.build();

复制代码

JMH支持以下5种格式的结果:

  • TEXT 导出文本文件。

  • CSV 导出csv格式文件。

  • SCSV 导出scsv等格式的文件。

  • JSON 导出成json文件。

  • LATEX 导出到latex,一种基于ΤΕΧ的排版系统。

一般来说,我们导出成CSV文件,直接在Excel中操作,生成相应的图形就可以了。

image.png

另外介绍几个可以做图的工具:

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

png](https://img-blog.csdnimg.cn/img_convert/cf5e1778d5b37581a0a5fc09737c4cf7.webp?x-oss-process=image/format,png)

另外介绍几个可以做图的工具:

[外链图片转存中…(img-Hx4jR6ap-1715623161109)]
[外链图片转存中…(img-JjZHd5bG-1715623161109)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 28
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我可以给你一个简单的示例来比较并行流和串行流在 jmh 测试中的性能差异。 假设我们有一个包含 1 百万个整数的列表,并且我们要计算这些数的总和。我们可以使用 Java 8 中的 Stream API 来实现这个任务,而且可以选择使用并行流或串行流来执行计算。 首先,我们来看一下串行流的实现代码: ```java public class SerialStreamBenchmark { private List<Integer> numbers; @Setup public void setup() { numbers = new ArrayList<>(); for (int i = 0; i < 1_000_000; i++) { numbers.add(i); } } @Benchmark public int sum() { return numbers.stream().mapToInt(Integer::intValue).sum(); } } ``` 在上述代码中,我们通过 `@Setup` 注解来初始化包含 1 百万个整数的列表。`@Benchmark` 注解标记了 `sum()` 方法,该方法将列表转换为流,然后通过 `mapToInt()` 方法将流中的元素映射为整数,并最终调用 `sum()` 方法计算它们的总和。 接下来,我们来看一下并行流的实现代码: ```java public class ParallelStreamBenchmark { private List<Integer> numbers; @Setup public void setup() { numbers = new ArrayList<>(); for (int i = 0; i < 1_000_000; i++) { numbers.add(i); } } @Benchmark public int sum() { return numbers.parallelStream().mapToInt(Integer::intValue).sum(); } } ``` 与串行流的实现代码类似,我们只需要将 `stream()` 方法替换为 `parallelStream()` 方法即可将流转换为并行流。 接下来,我们可以使用 jmh 测试这两个实现的性能差异。以下是测试类的代码: ```java public class StreamBenchmarkTest { @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.MILLISECONDS) @Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) @Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS) @Fork(1) @State(Scope.Thread) public static class SerialStreamBenchmarkTest { SerialStreamBenchmark serialStreamBenchmark = new SerialStreamBenchmark(); @Setup public void setup() { serialStreamBenchmark.setup(); } @Benchmark public int sum() { return serialStreamBenchmark.sum(); } } @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.MILLISECONDS) @Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) @Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS) @Fork(1) @State(Scope.Thread) public static class ParallelStreamBenchmarkTest { ParallelStreamBenchmark parallelStreamBenchmark = new ParallelStreamBenchmark(); @Setup public void setup() { parallelStreamBenchmark.setup(); } @Benchmark public int sum() { return parallelStreamBenchmark.sum(); } } public static void main(String[] args) throws RunnerException { Options options = new OptionsBuilder() .include(StreamBenchmarkTest.class.getSimpleName()) .build(); new Runner(options).run(); } } ``` 在上述代码中,我们使用了 `@BenchmarkMode`、`@OutputTimeUnit`、`@Warmup`、`@Measurement`、`@Fork` 和 `@State` 注解来配置 jmh 测试。其中,`@State` 注解指定了测试类的状态,`@Setup` 注解用于初始化测试数据。 最后,我们运行测试类,就可以得到并行流和串行流的性能测试结果了。 需要注意的是,实际的测试结果可能因为硬件环境和测试数据的不同而有所不同,这里只是提供了一个简单的示例来比较并行流和串行流在 jmh 测试中的性能差异。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值