Skip to content

Instantly share code, notes, and snippets.

@zhanhai
Last active October 21, 2020 02:26
Show Gist options
  • Save zhanhai/52a61b987176d82afbebe7aa2a0894eb to your computer and use it in GitHub Desktop.
Save zhanhai/52a61b987176d82afbebe7aa2a0894eb to your computer and use it in GitHub Desktop.

##Introducing JMH JMHs是由OpenJDK项目组提供的benchmark框架,它为我们编写和运行基于JVM的benchrmark提供了坚实可信的基础,使我们避免在benchmark时掉入之前说所的JVM优化陷阱中。 当然,JMH并没有提供免费的午餐,仍然需要我们在编写benchmark代码是尊守一定的规范和规则,但是JMH可以帮助我们更容易地解决问题 。

JMH Helloworld

我们先来看看如何在项目中引入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

JMH基本概念

上面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实现之前的测试

现在让我们基于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测试多线程并发场景。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment