JMH简介

JMH简介  JMH是新的microbenchmark(微基准测试)框架(2013年首次发布)。与其他众多框架相比它的特色优势在于,它是由Oracle实现JIT的相同人员开发的。特别是我想提一下AlekseyShipilev和他优秀的博客文章。JMH可能与最新的OracleJRE同步,其结果可信度很高。JMH的示例链接。使用JMH仅需满足2个必要条件(其他所有都是建议选项):设置jmh-…

大家好,又见面了,我是你们的朋友全栈君。

  JMH是新的microbenchmark(微基准测试)框架(2013年首次发布)。与其他众多框架相比它的特色优势在于,它是由Oracle实现JIT的相同人员开发的。特别是我想提一下Aleksey Shipilev和他优秀的博客文章。JMH可能与最新的Oracle JRE同步,其结果可信度很高。

JMH的示例链接

使用JMH仅需满足2个必要条件(其他所有都是建议选项):

  • 设置jmh-core的maven依赖
  • 使用@GenerateMicroBenchmark注解测试方法

本文将主要介绍JMH的基本规则和功能。第二篇文章将介绍JMH分析器

如何运行

在pom文件中加入依赖(在Maven Central查看jmh-core的最新版本):

1
2
3
4
5
6
7
<
dependencies
>
    
<
dependency
>
    
<
groupId
>org.openjdk.jmh</
groupId
>
    
<
artifactId
>jmh-core</
artifactId
>
    
<
version
>0.4.2</
version
>
    
</
dependency
>
</
dependencies
>

生成一个包含main方法的Java类。main方法中加入以下代码:

1
2
3
4
5
6
Options opt = new OptionsBuilder()
                
.include(".*" + YourClass.class.getSimpleName() + ".*")
                
.forks(1)
                
.build();
  
new Runner(opt).run();

测试方法使用@GenerateMicroBenchmark注解。运行该类。

测试模式

测试方法上@BenchmarkMode注解表示使用特定的测试模式:

名称 描述
Mode.Throughput 计算一个时间单位内操作数量
Mode.AverageTime 计算平均运行时间
Mode.SampleTime 计算一个方法的运行时间(包括百分位)
Mode.SingleShotTime 方法仅运行一次(用于冷测试模式)。或者特定批量大小的迭代多次运行(具体查看后面的“`@Measurement“`注解)——这种情况下JMH将计算批处理运行时间(一次批处理所有调用的总时间)
这些模式的任意组合 可以指定这些模式的任意组合——该测试运行多次(取决于请求模式的数量)
Mode.All

所有模式依次运行

 

时间单位

使用@OutputTimeUnit指定时间单位,它需要一个标准Java类型java.util.concurrent.TimeUnit作为参数。可是如果在一个测试中指定了多种测试模式,给定的时间单位将用于所有的测试(比如,测试SampleTime适宜使用纳秒,但是throughput使用更长的时间单位测量更合适)。

测试参数状态

测试方法可能接收参数。这需要提供单个的参数类,这个类遵循以下4条规则:

  • 有无参构造函数(默认构造函数)
  • 是公共类
  • 内部类应该是静态的
  • 该类必须使用@State注解

@State注解定义了给定类实例的可用范围。JMH可以在多线程同时运行的环境测试,因此需要选择正确的状态。

名称 描述
Scope.Thread 默认状态。实例将分配给运行给定测试的每个线程。
Scope.Benchmark 运行相同测试的所有线程将共享实例。可以用来测试状态对象的多线程性能(或者仅标记该范围的基准)。
Scope.Group 实例分配给每个线程组(查看后面的线程组部分)

除了将单独的类标记@State,也可以将你自己的benchmark类使用@State标记。上面所有的规则对这种情况也适用。

状态设置和清理

与JUnit测试类似,使用@Setup@TearDown注解标记状态类的方法(这些方法在JMH文档中称为fixtures)。setup/teardown方法的数量是任意的。这些方法不会影响测试时间(但是Level.Invocation可能影响测量精度)。

@Setup/@TearDown注解使用Level参数来指定何时调用fixture:

名称 描述
Level.Trial 默认level。全部benchmark运行(一组迭代)之前/之后
Level.Iteration 一次迭代之前/之后(一组调用)
Level.Invocation 每个方法调用之前/之后(不推荐使用,除非你清楚这样做的目的)

冗余代码

冗余代码消除是microbenchmark中众所周知的问题。通常的解决方法是以某种方式使用计算结果。JMH本身不会实施对冗余代码的消除。但是如果你想消除冗余代码——要做到测试程序返回值不为void永远返回你的计算结果。JMH将完成剩余的工作。

如果测试程序需要返回多个值,将所有这些返回值使用省时操作结合起来(省时是指相对于获取到所有结果所做操作的开销),或者使用BlackHole作为方法参数,将所有的结果放入其中(注意某些情况下BlockHole.consume可能比手动将结果组合起来开销更大)。BlackHole是一个thread-scoped类:

1
2
3
4
5
6
@GenerateMicroBenchmark
public void testSomething( BlackHole bh )
{
    
bh.consume( Math.sin( state_field ));
    
bh.consume( Math.cos( state_field ));
}

常量处理

如果计算结果是可预见的并且不依赖于状态对象,它可能被JIT优化。因此,最好总是从状态对象读取测试的输入并且返回计算的结果。这条规则大体上用于单个返回值的情形。使用BlackHole对象JVM更难优化它(但不是不可能被优化)。下面测试的所有方法都不会被优化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private double x = Math.PI;
  
@GenerateMicroBenchmark
public void bhNotQuiteRight( BlackHole bh )
{
    
bh.consume( Math.sin( Math.PI ));
    
bh.consume( Math.cos( Math.PI ));
}
  
@GenerateMicroBenchmark
public void bhRight( BlackHole bh )
{
    
bh.consume( Math.sin( x ));
    
bh.consume( Math.cos( x ));
}

返回单个值的情形更加复杂。下面的测试不会被优化,但是如果使用Math.log替换Math.sin,那么testWrong方法将被常量值替换。

1
2
3
4
5
6
7
8
9
10
11
12
13
private double x = Math.PI;
  
@GenerateMicroBenchmark
public double testWrong()
{
    
return Math.sin( Math.PI );
}
  
@GenerateMicroBenchmark
public double testRight()
{
    
return Math.sin( x );
}

因此,为使测试更可靠要严格遵守以下规则:永远从状态对象读取测试输入并返回计算的结果

循环

不要在测试中使用循环。JIT非常聪明,在循环中经常出现不可预料的处理。要测试真实的计算,让JMH处理剩余的部分。

在非统一开销操作情况下(比如测试处理列表的时间,这个列表在每个测试后有所增加),你可能使用@BenchmarkMode(Mode.SingleShotTime) 和@Measurement(batchSize = N)。但是不允许你自己实现测试的循环。

分支

默认JMH为每个试验(迭代集合)fork一个新的java进程。这样可以防止前面收集的“资料”——其他被加载类以及它们执行的信息对当前测试的影响。比如,实现了相同接口的两个类,测试它们的性能,那么第一个实现(目标测试类)可能比第二个快,因为JIT发现第二个实现类后就把第一个实现的直接方法调用替换为接口方法调用。

因此,不要把forks设为0除非你清楚这样做的目的

极少数情况下需要指定JVM分支数量时,使用@Fork对方法注解,就可以设置分支数量,预热(warmup)迭代数量和JVM分支的其他参数。

可能通过JMH API调用来指定JVM分支参数也有优势——可以使用一些JVM -XX:参数,通过JMH API访问不到它。这样就可以根据你的代码自动选择最佳的JVM设置(new Runner(opt).run()以简便的形式返回了所有的测试结果)。

编译器提示

可以为JIT提供关于如何使用测试程序中任何方法的提示。“任何方法”是指任何的方法——不仅仅是@GenerateMicroBenchmark注解的方法。使用@CompilerControl模式(还有更多模式,但是我不确定它们的有用程度):

名称 描述
CompilerControl.Mode.DONT_INLINE 该方法不能被内嵌。用于测量方法调用开销和评估是否该增加JVM的inline阈值
CompilerControl.Mode.INLINE 要求编译器内嵌该方法。通常与“`Mode.DONT_INLINE“`联合使用,检查内嵌的利弊。
CompilerControl.Mode.EXCLUDE 不编译该方法——解释它。在该JIT有多好的圣战中作为有用的参数:)

注解控制测试

通过注解指定JMH参数。这些注解用在类或者方法上。方法注解总是优先于类的注解。

名称 描述
@Fork 需要运行的试验(迭代集合)数量。每个试验运行在单独的JVM进程中。也可以指定(额外的)JVM参数。
@Measurement 提供真正的测试阶段参数。指定迭代的次数,每次迭代的运行时间和每次迭代测试调用的数量(通常使用@BenchmarkMode(Mode.SingleShotTime)测试一组操作的开销——而不使用循环)
@Warmup 与@Measurement相同,但是用于预热阶段
@Threads 该测试使用的线程数。默认是Runtime.getRuntime().availableProcessors()

CPU消耗

有时测试消耗一定CPU周期。通过静态的BlackHole.consumeCPU(tokens)方法来实现。Token是一些CPU指令。这样编写方法代码就可以达到运行时间依赖于该参数的目的(不被任何JIT/CPU优化)。

多参数的测试运行

很多情况下测试代码包含多个参数集合。幸运的是,要测试不同参数集合时JMH不会要求写多个测试方法。或者准确来说,测试参数是基本类型,基本包装类型或者String时,JMH提供了解决方法。

程序需要完成:

  1. 定义@State对象
  2. 在其中定义所有的参数字段
  3. 每个字段都使用@Param注解

@Param注解使用String数组作为参数。这些字符串在任何@Setup方法被调用前转换为字段类型。然而,JMH文档中声称这些字段值在@Setup方法中不能被访问。

JMH使用所有@Param字段的输出结果。因此,如果第一个字段有2个参数,第二个字段有5个参数,测试将运行2 * 5 * Forks次。

线程组——非统一的多线程

我们已经提到@State(Scope.Benchmark)用来测试多线程访问状态对象的情形。并发程度通过用来测试的线程数量设置。

可能也需要定义对状态对象非统一访问的情况——比如测试“读取——写入”场景时,读线程数通常高于写线程数量。JMH使用线程组来应对这种情形。

为设置测试组,需要:

  1. 使用@Group(name)注解标记所有的测试方法,为同一个组中的所有测试设置相同的名称(否则这些测试将独立运行——没有任何警告提示!)
  2. 使用@GroupThreads(threadsNumber)注解标记每个测试,指定运行给定方法的线程数量。

JMH将启动给定组的所有@GroupThreads,并发运行相同实验中同一组的所有测试。组和每个方法的结果将单独给出。

多线程——伪共享字段访问

你可能知道这样一个事实,大多数现代x86 CPU有64字节的cache line(缓存行)。CPU缓存提高了数据读取速率,但同时,如果你需要从多个线程同时读写两个邻近的字段,也会产生性能瓶颈。这种情况称为“伪共享”——字段似乎是独立访问的,但是实际上它们在硬件层面的相互竞争。

这个问题通常的解决方案是两边都增加至少128字节的虚拟数据。因为JVM可以将类的字段重排序,在相同的类内部增加可能不能正确运行。

更加健壮的方法是使用类层次——JVM通常将属于同一个类的字段放在一起。比如,定义类A有一个只读字段,类B继承类A且定义16个long字段,类C继承类B定义可写字段,最后类D继承类C定义另一个16个long字段——这就防止了被分配在下一个内存中对象的写变量竞争。

以防读写的字段类型相同,也可以使用两个数据位置相互距离很远的稀疏数组。在前面的情况中不要使用数组——它们是对象特定类型,仅需要增加4或8字节(取决于JVM设置)。

这个问题的另一种解决方法是如果你已经用到了Java 8:在可写字段上使用@sun.misc.Contended以及-XX:-RestrictContended的JVM设置。更多细节,参见Aleksey Shipilev的说明

JMH是如何解决竞争字段访问的呢?它在两边都增加了@State对象,但是这并不能在单一对象内部对个别的字段增加——需要自己来处理。

总结

  • JMH用于各种类型的microbenchmark——每个测试从纳秒到毫秒。它关注所有可测量的逻辑,测试人员只需编写测试方法的任务代码。JMH也包含对所有类型多线程测试的内在支持——统一(所有线程运行相同代码)和非统一(线程分组,每个组运行自己的代码)。
  • 如果仅仅一条规则需要记住的话,那就是——永远从@State对象读取测试输入并返回计算的结果(无论结果是明确的还是通过 BlackHole对象返回)

 

转载于:https://www.cnblogs.com/youqc/p/9341217.html

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/157591.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • 文本域[通俗易懂]

    文本域[通俗易懂]<textarea>元素用来创建多行文本框。与其他input元素不同,<textarea>元素并非空元素,因此它包含起始标签和结束标签。

  • consolewriteline用法_reviewmodule

    consolewriteline用法_reviewmodule严格模式ES6的模块自动采用严格模式,不管你有没有在模块头部加上"usestrict";。严格模式的限制如下变量必须声明后再使用函数的参数不能有同名属性,否则报错不能

  • 严苛模式(StrictMode)

    严苛模式(StrictMode)

    2021年11月29日
  • 数据结构教程 视频_神谷哲史男巫教程视频

    数据结构教程 视频_神谷哲史男巫教程视频史上最全的数据结构视频教程打包下载地址本文出自出自我是码农,转载请注明出处,谢谢!以下数据结构视频教程是我多年收集的,因为在百度网盘上分享整个教程很快就会被delete,所以我只好花费大量功夫对单个视频进行一个一个的分享,这样才能长时间保留下来,为了学习,麻烦些也值得了!现在毫无保留的免费共享给大家,与君共勉!个人认为数据结构这门课程比较难,看视频学习是最好的方式。当年我就是看了多个视频,然…

    2022年10月12日
  • 数据挖掘的方法有哪些?

    数据挖掘的方法有哪些?数据挖掘的方法有哪些?01数据挖掘方法分类介绍1.预测性——有监督学习2.描述性——无监督学习02数据挖掘方法论1.CRISP-DM方法论2.SEMMA方法论03数据挖掘建模框架的3个原则1.以成本-收益分析为单一分析框架2.以分析主体和客体为视角3.构建全模型生命周期工作模板数据挖掘是一个多学科交叉的产物,涉及统计学、数据库、机器学习、人工智能及模式识别等多种学科,如图1-4所示。01数据挖掘方法分类介绍数据挖掘方法按照来源进行分类显得过于庞杂,而且不便于理解和记忆。按照其目

  • rowBounds_robocopy用法

    rowBounds_robocopy用法generator添加

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号