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对象返回)。

JMH简介的更多相关文章

  1. 性能调优必备利器之 JMH

    if 快还是 switch 快?HashMap 的初始化 size 要不要指定,指定之后性能可以提高多少?各种序列化方法哪个耗时更短? 无论出自何种原因需要进行性能评估,量化指标总是必要的. 在大部分 ...

  2. (八)使用 jmh 压测 Dubbo

    1.JMH简介 JMH即Java Microbenchmark Harness,是Java用来做基准测试的一个工具,该工具由OpenJDK提供并维护,测试结果可信度高. 相对于 Jmeter.ab , ...

  3. 【Java必修课】ArrayList与HashSet的contains方法性能比较(JMH性能测试)

    1 简介 在日常开发中,ArrayList和HashSet都是Java中很常用的集合类. ArrayList是List接口最常用的实现类: HashSet则是保存唯一元素Set的实现. 本文主要对两者 ...

  4. JMH 使用指南

    简介 JMH(Java Microbenchmark Harness)是用于代码微基准测试的工具套件,主要是基于方法层面的基准测试,精度可以达到纳秒级.该工具是由 Oracle 内部实现 JIT 的大 ...

  5. ASP.NET Core 1.1 简介

    ASP.NET Core 1.1 于2016年11月16日发布.这个版本包括许多伟大的新功能以及许多错误修复和一般的增强.这个版本包含了多个新的中间件组件.针对Windows的WebListener服 ...

  6. MVVM模式和在WPF中的实现(一)MVVM模式简介

    MVVM模式解析和在WPF中的实现(一) MVVM模式简介 系列目录: MVVM模式解析和在WPF中的实现(一)MVVM模式简介 MVVM模式解析和在WPF中的实现(二)数据绑定 MVVM模式解析和在 ...

  7. Cassandra简介

    在前面的一篇文章<图形数据库Neo4J简介>中,我们介绍了一种非常流行的图形数据库Neo4J的使用方法.而在本文中,我们将对另外一种类型的NoSQL数据库——Cassandra进行简单地介 ...

  8. REST简介

    一说到REST,我想大家的第一反应就是“啊,就是那种前后台通信方式.”但是在要求详细讲述它所提出的各个约束,以及如何开始搭建REST服务时,却很少有人能够清晰地说出它到底是什么,需要遵守什么样的准则. ...

  9. Microservice架构模式简介

    在2014年,Sam Newman,Martin Fowler在ThoughtWorks的一位同事,出版了一本新书<Building Microservices>.该书描述了如何按照Mic ...

随机推荐

  1. 使用screen管理后台程序

    我们常需要SSH 或者telent 远程登录到Linux 服务器,经常运行一些需要很长时间才能完成的任务,在此期间不能关掉窗口或者断开连接,否则这个任务就会被杀掉,一切半途而废了.这时,我们可以用sc ...

  2. ES6生成器与迭代器

    ES6迭代器的一个例子 function run(taskDef) { var task = taskDef(); var result = task.next(); // 递归执行迭代 functi ...

  3. IntelliJ IDLE

    目录 IntelliJ IDLE java项目结构 代码生成 psvm sout 快捷键 更改代码提示快捷键 代码相关快捷键 窗口相关快捷键 Debug快捷键 主题导入 下载主题 导入主题 应用主题 ...

  4. PythonDay06

    第六章 今日内容 小数据池 深浅拷贝 集合 小数据池 == is id == 判断两个值是否相等 is --- 是 判断两个值的内存地址是否相等 代码块:一个py文件,一个函数,一个模块,终端中每一行 ...

  5. Lucene整理--中文分词

    版权声明:本文为博主原创文章,未经博主同意不得转载. https://blog.csdn.net/hai_cheng001/article/details/37511379 看lucene主页(htt ...

  6. Paper Reading

    Paper Reading_SysML Paper Reading_Computer Architecture Paper Reading_Database Paper Reading_Distrib ...

  7. String.Net “System.TypeInitializationException”类型的未经处理的异常在 Spring.NetDemo.exe 中发生

    今天编写String.Net时,遇到“System.TypeInitializationException”类型的未经处理的异常在 Spring.NetDemo.exe 中发生 原因配置文件的顺序写错 ...

  8. js的抖动及防抖和节流

     js的抖动 在 js 中 改变窗口大小 & 上下滚动滚动条 & 反复向输入框中输入内容 ... , 如果绑定了相应的事件 , 这些事件的触发频率非常高, 严重影响用户体验和服务器的性 ...

  9. intellij idea 的快捷键方法

    1.Ctrl+N按名字搜索类 相当于eclipse的ctrl+shift+R,输入类名可以定位到这个类文件,就像idea在其它的搜索部分的表现一样,搜索类名也能对你所要搜索的内容多个部分进行匹配,而且 ...

  10. SPSS 23下载安装和激活

    目录 1. 其他版本 2. 安装教程 3. 下载地址 1. 其他版本 参考:https://www.cnblogs.com/coco56/p/11648399.html 2. 安装教程 SPSS 23 ...