##Introducing JMH JMHs是由OpenJDK项目组提供的benchmark框架,它为我们编写和运行基于JVM的benchrmark提供了坚实可信的基础,使我们避免在benchmark时掉入之前说所的JVM优化陷阱中。 当然,JMH并没有提供免费的午餐,仍然需要我们在编写benchmark代码是尊守一定的规范和规则,但是JMH可以帮助我们更容易地解决问题 。
我们先来看看如何在项目中引入JMH,并尝试基于JMH实现第一个benchmark。
假定我们的项目使用gradle构建(maven、ant也有类似的方法引入JMH,这里不再介绍),首先我们需要使用JMH插件,在build.gradle
中添加:
plugins {
id 'me.champeau.gradle.jmh' version '0.3.0'
}
apply plugin: 'me.champeau.gradle.jmh'
该插件将会从src\jmh
目录读取benchmark相关的代码和资源,因此我们需要手工建立相关目录:
src/jmh
|- java : java sources for benchmarks
|- resources : resources for benchmarks
插件提供了jmh相关任务,我们通过执行 gradle jmh
执行benchmark测试。
我们先写一个最简单的benchmark:
package inf.demo.jmh.hello;
import org.openjdk.jmh.annotations.*;
import java.util.concurrent.TimeUnit;
@State(Scope.Thread)
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
@Fork(
value = 3,
jvmArgsAppend = {"-server", "-disablesystemassertions"}
)
public class JMHHelloWorld {
@Benchmark
@Warmup(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS)
public void helloWorld() {
}
}
然后运行gradle jmh
,结果如下:
# Run progress: 0.00% complete, ETA 00:01:03
# Fork: 1 of 3
# Warmup Iteration 1: 3367094999.052 ops/s
Iteration 1: 3365894259.726 ops/s
Iteration 2: 3389021153.912 ops/s
Iteration 3: 3386021239.790 ops/s
Iteration 4: 3374184071.155 ops/s
Iteration 5: 3392628368.231 ops/s
Iteration 6: 3371706993.604 ops/s
Iteration 7: 3407591611.061 ops/s
Iteration 8: 3414140943.737 ops/s
Iteration 9: 3344801926.902 ops/s
Iteration 10: 3373951730.486 ops/s
# Run progress: 33.33% complete, ETA 00:00:42
# Fork: 2 of 3
# Warmup Iteration 1: 3374615325.863 ops/s
Iteration 1: 3397052216.650 ops/s
Iteration 2: 3388236587.773 ops/s
Iteration 3: 3400051169.649 ops/s
Iteration 4: 3389341722.823 ops/s
Iteration 5: 3401696642.812 ops/s
Iteration 6: 3353787167.095 ops/s
Iteration 7: 3377889183.971 ops/s
Iteration 8: 3353963984.630 ops/s
Iteration 9: 3341355755.834 ops/s
Iteration 10: 3398196094.876 ops/s
# Run progress: 66.67% complete, ETA 00:00:21
# Fork: 3 of 3
# Warmup Iteration 1: 3362927493.612 ops/s
Iteration 1: 3317078197.597 ops/s
Iteration 2: 3405552760.327 ops/s
Iteration 3: 3400991747.882 ops/s
Iteration 4: 3388531226.841 ops/s
Iteration 5: 3377869112.978 ops/s
Iteration 6: 3378812074.134 ops/s
Iteration 7: 3382823471.537 ops/s
Iteration 8: 3391236263.479 ops/s
Iteration 9: 3396385841.186 ops/s
Iteration 10: 3388684991.377 ops/s
Result "helloWorld":
3383211326.394 ±(99.9%) 10630059.980 ops/s [Average]
(min, avg, max) = (3313246332.181, 3383211326.394, 3414140943.737), stdev = 23775654.173
CI (99.9%): [3372581266.415, 3393841386.374] (assumes normal distribution)
# Run complete. Total time: 00:01:03
Benchmark Mode Cnt Score Error Units
JMHHelloWorld.helloWorld thrpt 30 3398777965.337 ± 30370988.187 ops/s
Benchmark result is saved to build\reports\jmh\results.txt
上面HelloWorld例子有一些JMHde 基本概念,这里做个简单说明。
- BenchmarkMode
JMH支持多种BechmarkMode,用于评估不同指标,主要包括:
/**
* <p>Throughput: 单位时间内执行的操作个数,最常见的就是TPS.</p>
*
* <p>JMH会在一定的时间内不断执行被测试的动作,然后统计执行次数.</p>
*/
Throughput,
/**
* <p>每个操作的平均耗时</p>
*
* <p>运行方式和Throughput类似,其实该值就是Throughtput的倒数</p>
*/
AverageTime,
/**
* <p>通过抽样得到的操作执行时间</p>
*
* <p>JMH在指定期限内不断执行,期间对操作执行时间进行随机采样</p>
*/
SampleTime
-
State
State用于封装benchmark依赖的数据和对象,JMH会自动将benchmark需要的State注入到执行方法中。State有不同的作用域,比如:整个应用,或者单个benchmark,或者一次调用。 -
Fork/Iteration
Fork和Iterator定义了benchmark的执行次数。其中,Fork
表示将为每个benchmark启动多少轮独立的JVM子进程,在每次Fork
后,benchmark将执行Iterator
次测试。 -
Warmup/Measurement
JMH把benchmark分成两个阶段:Warmup和Measurement,前者表示热身,其运行期间的数据将不被统计到结果中,后者才是正式的测试。之所以这么划分,是希望通过Warmup,过滤掉加载bytecode到JIT编译这段时间的测试。
现在让我们基于JMH实现之前的测试。实现很简单:
/**
* 不执行任何操作,其性能最高。
*/
@Benchmark
public void baseline_return_void() {
}
/**
* 只返回常量,由于有返回值,所以和上一个benchmark相比,性能下降一个数量级
*/
@Benchmark
public double baseline_return_zero() {
return 0.0;
}
/**
* 调用外部的常量函数,没有执行任何其他计算,性能和上面接近。
*/
@Benchmark
public double constant(Data data) {
return DistanceAlgo.constant(data.x1, data.y1, data.x2, data.y2);
}
/**
* 正常测试。性能和其他benchmark比较起来最低。
*/
@Benchmark
public double distance(Data data) {
return DistanceAlgo.distance(data.x1, data.y1, data.x2, data.y2);
}
/**
* 也调用计算方法,但是由于使用常量调用函数,会触发JVM优化,优化后的代码不再执行计算而是直接返回结果,所以性能必上面的正常计算要高。
*/
@Benchmark
public double distance_folding(){
return DistanceAlgo.distance(0.0d, 0.0d, 10.0d, 10.0d);
}
/**
* 由于没有返回值,即使在JMH中也被DCE,所以性能要远远高于正常计算,和第一个不执行任何操作的benchmark性能接近。
*/
@Benchmark
public void distance_deadcode(Data data) {
DistanceAlgo.distance(data.x1, data.y1, data.x2, data.y2);
}
/**
* 既没有返回值,又使用常量直接调用计算方法,性能比上面DCE的benchmark还要高一些。
*/
@Benchmark
public void distance_deadcode_and_folding() {
DistanceAlgo.distance(0.0, 0.0, 10.0, 10.0);
}
在上面的代码中,我们比较多个不同的benchmark,最终结果如下:
JMHDistanceBenchmark.baseline_return_void thrpt 30 3381788961.258 49249314.613 ops/s
JMHDistanceBenchmark.baseline_return_zero thrpt 30 416398598.397 ± 2484292.696 ops/s
JMHDistanceBenchmark.constant thrpt 30 414428330.515 ± 3416776.566 ops/s
JMHDistanceBenchmark.distance thrpt 30 243567984.872 ± 2842917.637 ops/s
JMHDistanceBenchmark.distance_folding thrpt 30 403182310.639 ± 4784440.465 ops/s
JMHDistanceBenchmark.distance_deadcode thrpt 30 3391086798.787 ± 25705423.784 ops/s
JMHDistanceBenchmark.distance_deadcode_and_folding thrpt 30 3420206731.341 ± 23167062.754 ops/s
从结果可以看出,即使在JMH中,我们也没有万无一失,仍然有可能掉到坑里。建议大家仔细阅读JMH提供的SAMPLE, 这些例子有大量的注释,可以作为很好的JMH学习例子。
JMH为多线程并发环境下的测试也提供了强大的支持,后面我们将介绍如何使用JMH测试多线程并发场景。