JMH如何使用

目录JMH如何使用JMH的基本用法基础注解 @BenchmarkWarmup以及Measurement全局设置局部设置(基准测试方法之上)BenchmarkMode四种模式概念多模式设置OutputTimeUnit三种State的使用Thread独享(共享)的State线程组共享的State@Param的使用JMH的测试套件Setup以及TearDown使用Level控制测试套件

JMH的基本用法

基础注解 @Benchmark

在一个基本测试类中至少包含一个被@Benchmark标记的方法,否则会抛出异常。

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Thread)
public class JMHExample02 {

    public void normalMethod() {
    }

    public static void main(String[] args) throws RunnerException {
        final Options opts = new OptionsBuilder().include(JMHExample02.class.getSimpleName())
                .forks(1)
                .measurementIterations(10)
                .warmupIterations(10)
                .build();
        new Runner(opts).run();
    }
}

异常:Exception in thread "main" No benchmarks to run; check the include/exclude regexps.

Warmup以及Measurement

Warmup: “热身”,使得在度量之前,类经历了早期的优化、JVM运行期编译、JIT优化

Measurement: 真正的度量操作,度量过程中的所有数据都会被纳入统计

全局设置

构造Options时设置批次执行

final Options opts = new OptionsBuilder().include(JMHExample02.class.getSimpleName())
                .forks(1)
   				//度量执行批次为5
                .measurementIterations(5)
    			//在度量之前先执行两次热身
                .warmupIterations(2)
                .build();
        new Runner(opts).run();

@Warmup与@Measurement注解

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Thread)

@Measurement(iterations = 5)
@Warmup(iterations = 2)
public class JMHExample02

局部设置(基准测试方法之上)

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Thread)
@Measurement(iterations = 5)
@Warmup(iterations = 3)
public class JMHExample03 {

    @Benchmark
    public void test1() throws InterruptedException {
        TimeUnit.MILLISECONDS.sleep(10);
    }

    @Benchmark
    @Warmup(iterations = 5)
    @Measurement(iterations = 4)
    public void test2() throws InterruptedException {
        TimeUnit.MILLISECONDS.sleep(1);
    }
    public static void main(String[] args) throws RunnerException {
        final Options opts = new OptionsBuilder().include(JMHExample03.class.getSimpleName())
                .forks(1)
            	//.measurementIterations(2)
                //.warmupIterations(2)
                .build();
        new Runner(opts).run();
    }
}

执行结果:

基准测试方法 测试结果
test1 预热3次、度量5次
test2 预热5次、度量4次

Warmup以及Measurement在基准测试方法上的设置会覆盖全局设置,但是无法覆盖Options中构建的全局设置

BenchmarkMode

四种模式概念

模式 使用
AverageTime 平均响应时间(方法每一次调用)
Throughput 单位时间内方法的调用次数
SampleTime 抽样的方式统计性能数据
SingleShotTime 无论warmup还是measurement,每批次中,基准测试方法只会执行一次(warmup一般设置为0)

多模式设置

我们可以在基准测试方法上设置多个模式,甚至是全部

@BenchmarkMode({Mode.AverageTime,Mode.Throughput})
@Benchmark
public void testThroughputAndAverageTime(){
	TimeUnit.MILLISECONDS.sleep(1);
}
@BenchmarkMode(Mode.All)
@Benchmark
public void testAll(){
	TimeUnit.MILLISECONDS.sleep(1);
}

覆盖次序:基准方法上的设置会覆盖类上的设置,Options上的设置会覆盖所有的设置。

OutputTimeUnit

提供了统计结果输出时的时间单位,覆盖次序与BenchmarkMode一致

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Thread)
@Measurement(iterations = 5)
@Warmup(iterations = 3)
public class JMHExample04 {

    @OutputTimeUnit(TimeUnit.MICROSECONDS)
    @Benchmark
    public void test() throws InterruptedException {
        TimeUnit.SECONDS.sleep(1);
    }

    public static void main(String[] args) throws RunnerException {
        final Options opts = new OptionsBuilder().include(JMHExample04.class.getSimpleName())
                .include(JMHExample04.class.getSimpleName())
                .timeUnit(TimeUnit.NANOSECONDS)
                .forks(1)
                .build();
        new Runner(opts).run();
    }
}

三种State的使用

Thread独享(共享)的State

Scope.Thread: 每个运行基准测试方法的线程都拥有一个独立的对象实例
Scope.Benchmark: 多个线程共享同一个对象实例

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Fork(1)
@Measurement(iterations = 5)
@Warmup(iterations = 3)
//设置5个线程运行基准测试方法
@Threads(5)
public class JMHExample07 {

    @State(Scope.Benchmark)
    //@State(Scope.Benchmark)
    public static class Test {
        public Test() {
            System.out.println("create instance");
        }

        public void method() {
        }
    }

    @Benchmark
    public void test(Test test){
        test.method();
    }

    public static void main(String[] args) throws RunnerException {
        final Options opts = new OptionsBuilder()
                .include(JMHExample07.class.getSimpleName())
                .build();
        new Runner(opts).run();
    }
}

执行结果

​ @State(Scope.Thread)输出了5次 “create instance”

​ @State( Scope.Benchmark)只输出了1次 “create instance”

上述的“对象”是指被@State标记的类实例。可通过基准测试方法的参数引入(如上),或是直接运行基准测试方法所在的宿主class。

@Thread注解设置参与基准测试的线程数

线程组共享的State

Scope.Group

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Fork(1)
@Measurement(iterations = 5)
@Warmup(iterations = 3)
//设置5个线程运行基准测试方法
@Threads(5)
public class JMHExample08 {

    @State(Scope.Group)
    public static class Test {
        public Test() {
            System.out.println("create instance");
        }

        public void read() {
            System.out.println("test read");
        }

        public void write() {
            System.out.println("test write");
        }
    }

    @GroupThreads(3)
    @Group("test")
    @Benchmark
    public void testRead(Test test){
        test.read();
    }

    @GroupThreads(3)
    @Group("test")
    @Benchmark
    public void testWrite(Test test){
        test.write();
    }
    public static void main(String[] args) throws RunnerException {
        final Options opts = new OptionsBuilder()
                .include(JMHExample08.class.getSimpleName())
                .build();
        new Runner(opts).run();
    }
}

执行结果:testRead()与testWrite()交替执行

前两种State的情况下,基准测试方法都只能按照顺序逐个执行。而想要多个方法并行地去访问共享数据,则需要Scope.Group

@Param的使用

假如我们现在想要对两个不同类型的Map进行微基准的性能测试,该怎么做呢?按照前面的方法,我们可以为两个Map分别编写微基准测试方法,如下:

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Fork(1)
@Measurement(iterations = 5)
@Warmup(iterations = 3)
//设置5个线程运行基准测试方法
@Threads(5)
@State(Scope.Benchmark)
public class JMHExample09 {

    private Map<Long, Long> concurrentHashMap;
    private Map<Long, Long> synchronizedMap;
    @Setup
    public void setUp(){
        concurrentHashMap = new ConcurrentHashMap<>();
        synchronizedMap = Collections.synchronizedMap(new HashMap<Long, Long>());
    }

    @Benchmark
    public void testConcurrentHashMap(){
        this.concurrentHashMap.put(System.nanoTime(),System.nanoTime());
    }

    @Benchmark
    public void testSynchronizedMap(){
        this.concurrentHashMap.put(System.nanoTime(),System.nanoTime());
    }
    public static void main(String[] args) throws RunnerException {
        final Options opts = new OptionsBuilder()
                .include(JMHExample09.class.getSimpleName())
                .build();
        new Runner(opts).run();
    }
}

但是当我们想对更多类型的集合(或是其他的东东)进行微基准测试时,这种方法显然就多了很多的冗余代码,此时我们就可以使用@Param来简化代码啦,如下:

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Fork(1)
@Measurement(iterations = 5)
@Warmup(iterations = 3)
//设置5个线程运行基准测试方法
@Threads(5)
//多个线程共享实例
@State(Scope.Benchmark)
public class JMHExample10 {

    @Param({"1","2","3","4"})
    private int type;

    Map<Object, Object> map = null;
    @Setup
    public void setUp(){
        switch (type){
            case 1:
                this.map = new ConcurrentHashMap<>();
                break;
            case 2:
                this.map = new ConcurrentSkipListMap<>();
                break;
            case 3:
                this.map = new Hashtable<>();
                break;
            case 4:
                this.map = Collections.synchronizedMap(new HashMap<>());
        }
    }

    @Benchmark
    public void test(){
        this.map.put(System.nanoTime(),System.nanoTime());
    }
    public static void main(String[] args) throws RunnerException {
        final Options opts = new OptionsBuilder()
                .include(JMHExample10.class.getSimpleName())
                .build();
        new Runner(opts).run();
    }
}

执行结果

结果中只截取了部分关键输出,多出的type列正是对应@Param所提供的参数

Benchmark                           (type)  Mode  Cnt       Score        Error  Units
JMHExample09.testConcurrentHashMap       1  avgt    5   23098.216 ± 161361.562  us/op
JMHExample09.testConcurrentHashMap       2  avgt    5   45467.394 ± 103183.828  us/op
JMHExample09.testConcurrentHashMap       3  avgt    5   61373.892 ± 243766.954  us/op
JMHExample09.testConcurrentHashMap       4  avgt    4  140614.207 ± 650830.876  us/op

各个字段含义

Mode Cnt Score Error Units type
模式(四种) 基准方法调用次数 响应时间 时间偏差 时间单位 @Param参数

有了@Param之后,我们只需要编写一次微基准测试方法即可,JMH会根据@Param提供的参数值自动执行基准测试以及统计。

JMH的测试套件

Setup以及TearDown

@Setup: 基准测试方法之前调用,通常用于资源的初始化。
@TearDown: 基准测试方法之后调用,通常用于资源的回收清理工作。

@BenchmarkMode(Mode.SingleShotTime)
@Fork(1)
@Warmup(iterations = 5)
@Measurement(iterations = 10)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Thread)
public class JMHExample11 {

    private List<String> list = null;
    @Setup
    public void setUp(){
        this.list = new ArrayList<>();
        System.out.println("setUp...");
    }

    @Benchmark
    public void testRight() {
        this.list.add("Test");
    }

    @Benchmark
    public void testWrong() {
        //do nothing here
    }

    @TearDown
    public void tearDown() {
        System.out.println("tearDown...");
        assert this.list.size() > 0 : "The Size  Of List Must Lager Than Zero";
    }

    public static void main(String[] args) throws RunnerException {
        final Options opts = new OptionsBuilder()
                .include(JMHExample11.class.getSimpleName())
                .jvmArgs("-ea") //enable assertion 激活断言
                .build();
        new Runner(opts).run();
    }
}

执行结果

# Warmup Iteration   1: setUp...
4.900 us/op
# Warmup Iteration   2: 0.600 us/op
# Warmup Iteration   3: 0.600 us/op
# Warmup Iteration   4: 0.400 us/op
# Warmup Iteration   5: 0.500 us/op
Iteration   1: 0.600 us/op
Iteration   2: 0.900 us/op
Iteration   3: 0.600 us/op
Iteration   4: 0.400 us/op
Iteration   5: 0.500 us/op
Iteration   6: 0.400 us/op
Iteration   7: 0.300 us/op
Iteration   8: 0.500 us/op
Iteration   9: 0.400 us/op
Iteration  10: tearDown...

使用Level控制测试套件

通过上面的结果可以知道,默认情况下,@Setup@TearDown的套件方法分别在所有执行批次之前与之后执行。JMH还提供了另外两种配置。

Trial:默认配置。@Setup(Level.Trial)

Iteration:因为可以设置Warmup与Measurement,所以基准方法可能被执行若干个批次。Iteration将允许我们在每个批次前后执行套件方法。

@Setup(Level.Iteration)
public void setUp()

Invocation:每个批次(Warmup或Measurement)中都可能存在多次基准方法的调用,Invocation将允许我们在每一次调用基准方法的前后执行套件。

@Setup(Level.Invocation)
public void setUp()

具体代码不做展示。