• JMH: 最装逼,最牛逼的基准测试工具套件


    JMH简介

    官网:http://openjdk.java.net/projects/code-tools/jmh/

    简介:JMH is a Java harness for building, running, and analysing nano/micro/milli/macro benchmarks written in Java and other languages targetting the JVM,由简介可知,JMH不止能对Java语言做基准测试,还能对运行在JVM上的其他语言做基准测试。而且可以分析到纳秒级别。

    推荐用法

    官方推荐创建一个独立的Maven工程来运行JMH基准测试,这样更能确保结果的准确性。当然也可以在已存在的工程中,或者在IDE上运行,但是越复杂,结果越不可靠(more complex and the results are less reliable)。

    简单实用

    推荐用法通过命令行创建,构建和运行JMH基准测试。

    setup

    生成一个新的JMH工程的maven命令如下:

     mvn archetype:generate 
     -DinteractiveMode=false 
     -DarchetypeGroupId=org.openjdk.jmh 
     -DarchetypeArtifactId=jmh-java-benchmark-archetype 
     -DgroupId=com.afei.jmh 
     -DartifactId=jmh 
     -Dversion=1.0.0-SNAPSHOT
    

    执行该命令后,会创建一个Maven工程,但是默认生成的MyBenchmark.java并没有在预期的包名com/afei/jmh中,即使加上参数-DpackageName=com.afei.jmh也不行,只能先手动将其挪到包名下,这里作为一个小小的遗留问题。

    压测代码

    默认生成的MyBenchmark.java源码如下,testMethod()中就是你要压测的代码,下面是笔者要压测的洗牌算法:

    @Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
    @Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
    @Fork(1)
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.NANOSECONDS)
    public class MyBenchmark {
    
        @GenerateMicroBenchmark
        public List<Integer> testMethod() {
            int cardCount = 54;
            List<Integer> cardList = new ArrayList<Integer>();
            for (int i=0; i<cardCount; i++){
                cardList.add(i);
            }
            // 洗牌算法
            Random random = new Random();
            for (int i=0; i<cardCount; i++) {
                int rand = random.nextInt(cardCount);
                Collections.swap(cardList, i, rand);
            }
            return cardList;
        }
    
    }
    

    build

    写完代码接下来就是构建并打包,在pom.xml所在目录执行如下命令:

    mvn clean package
    

    说明:这一步,也可以通过IDE工具构建打包。

    running

    打包成功后在target目录下生成了一个JAR文件:microbenchmarks.jar,需要注意的是,官网的运行命令是java -jar target/benchmarks.jar,至于到底是benchmarks.jar还是microbenchmarks.jar,取决于你的POM文件:

    <configuration>
        <finalName>microbenchmarks</finalName>
        <transformers>
            <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                <mainClass>org.openjdk.jmh.Main</mainClass>
            </transformer>
        </transformers>
    </configuration>
    

    笔者生成的maven工程是microbenchmarks,所以,运行时执行如下命令:

    java -jar target/microbenchmarks.jar
    

    输出结果如下:

    # Run progress: 0.00% complete, ETA 00:00:10
    # VM invoker: C:Program FilesJavajre1.8.0_181injava.exe
    # VM options: <none>
    # Fork: 1 of 1
    # Warmup: 5 iterations, 1 s each
    # Measurement: 5 iterations, 1 s each
    # Threads: 1 thread, will synchronize iterations
    # Benchmark mode: Average time, time/op
    # Benchmark: com.afei.jmh.MyBenchmark.testMethod
    # Warmup Iteration   1: 1133.738 ns/op
    # Warmup Iteration   2: 1169.750 ns/op
    # Warmup Iteration   3: 1066.204 ns/op
    # Warmup Iteration   4: 1086.300 ns/op
    # Warmup Iteration   5: 1145.228 ns/op
    Iteration   1: 1045.157 ns/op
    Iteration   2: 1064.303 ns/op
    Iteration   3: 1064.227 ns/op
    Iteration   4: 1053.979 ns/op
    Iteration   5: 1055.718 ns/op
    
    Result : 1056.677  ±(99.9%) 30.809 ns/op
      Statistics: (min, avg, max) = (1045.157, 1056.677, 1064.303), stdev = 8.001
      Confidence interval (99.9%): [1025.868, 1087.486]
    
    Benchmark                        Mode   Samples         Mean   Mean error    Units
    c.a.j.MyBenchmark.testMethod     avgt         5     1056.677       30.809    ns/op
    
    

    结果解读

    下面对输出结果一些重要信息进行解读:

    @Warmup

    由于笔者加了这个注解:@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)。所以,基准测试后对代码预热总计5秒(迭代5次,每次1秒)。预热对于压测来说非常非常重要,如果没有预热过程,压测结果会很不准确。这个注解对应的日志如下:

    # Warmup Iteration   1: 1133.738 ns/op
    # Warmup Iteration   2: 1169.750 ns/op
    # Warmup Iteration   3: 1066.204 ns/op
    # Warmup Iteration   4: 1086.300 ns/op
    # Warmup Iteration   5: 1145.228 ns/op
    

    @Measurement

    另外一个重要的注解:@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS),表示循环运行5次,总计5秒时间。

    @Fork

    这个注解表示fork多少个线程运行基准测试,如果@Fork(1),那么就是一个线程,这时候就是同步模式。

    @BenchmarkMode&@OutputTimeUnit

    基准测试模式申明为:@BenchmarkMode(Mode.AverageTime)搭配@OutputTimeUnit(TimeUnit.NANOSECONDS)(可选基准测试模式通过枚举Mode得到),笔者的示例是AverageTime,即表示每次操作需要的平均时间,而OutputTimeUnit申明为纳秒,所以基准测试单位是ns/op,即每次操作的纳秒单位平均时间。基准测试结果如下:

    Result : 1056.677 ±(99.9%) 30.809 ns/op
      Statistics: (min, avg, max) = (1045.157, 1056.677, 1064.303), stdev = 8.001
      Confidence interval (99.9%): [1025.868, 1087.486]
    

    最后一段结果如下,重点关注MeanUnits两个字段,组合起来就是1227.928ns/op,即每次操作耗时1056.677纳秒:

    Benchmark                        Mode   Samples         Mean   Mean error    Units
    c.a.j.MyBenchmark.testMethod     avgt         5     1056.677       30.809    ns/op
    

    如果我们将@BenchmarkMode(Mode.AverageTime)@OutputTimeUnit(TimeUnit.NANOSECONDS)的组合,改成@BenchmarkMode(Mode.Throughput)@OutputTimeUnit(TimeUnit.MILLISECONDS),那么基准测试结果就是每毫秒的吞吐量(即每毫秒多少次操作),结果如下,表示943.437ops/ms:

    Benchmark                        Mode   Samples         Mean   Mean error    Units
    c.a.j.MyBenchmark.testMethod    thrpt         5      943.437       44.060   ops/ms
    

    Mean error表示误差,或者波动,与Result的±值对应:Result : 1056.677 ±(99.9%) 30.809 ns/op

    基准测试对比

    将自定义洗牌算法和JDK原生的洗牌算法Collections.shuffle(cardList);进行基准测试对比,结果如下:

    -ops/msns/op
    JDK原生洗牌算法 807.470 1149.900
    自定义洗牌算法(for循环外面new Random) 943.437 1056.677
    自定义洗牌算法(for循环里面new Random) 300.467 3346.509

    Random在for循环里面的源码如下:

    for (int i=0; i<cardCount; i++) {
        Random random = new Random();
        int rand = random.nextInt(cardCount);
        Collections.swap(cardList, i, rand);
    }
    

    说明,自定义洗牌算法事实上就是JDK自带洗牌算法中集合的size少于SHUFFLE_THRESHOLD(这个值为5)时的实现。另外,由基准测试对比可知,for循环里面不断new Random的性能相比只在for循环外面new Random一次的性能要差好3倍左右。

    另外,在集合的size超过SHUFFLE_THRESHOLD即5后JDK原生洗牌算法,相比size少于该值得洗牌算法性能并没有提高,两者性能差在10%左右,基本可以忽略。这里想不明白,JDK原生洗牌算法在集合的size超过SHUFFLE_THRESHOLD的优化的意思,当然也可能跟笔者基准测试样本有关(毕竟笔者只测试了size为54的集合)。

    JMH和jMeter的不同

    JMH和jMeter的使用场景还是有很大的不同的,jMeter更多的是对rest api进行压测,而JMH关注的粒度更细,它更多的是发现某块性能槽点代码,然后对优化方案进行基准测试对比。比如json序列化方案对比,bean copy方案对比,文中提高的洗牌算法对比等。

    案例参考

    官方给了很多样例代码,有兴趣的同学可以自己查询并学习JMH:http://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/



    作者:阿飞的博客
    链接:https://www.jianshu.com/p/0da2988b9846
    来源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
  • 相关阅读:
    docker in docker
    docker社区的geodata/gdal镜像dockerfile分析
    howto:在构建基于debian的docker基础镜像时,更换国内包源
    使用Visual Studio 2017构建.Net Core的Docker镜像
    步骤:asp.net core中使用identifyserver4颁发令牌
    部署:阿里云ECS部署Docker CE
    问题:调用 ASP.Net Core WebAPI的HTTP POST方法时,从 [FromBody] 中读取的 MongoDB GeoJsonObjectModel成员总是null
    《.NET 微服务:适用于容器化 .NET 应用的体系结构》关键结论
    SQL数据库注入防范 ASP.NET Globle警告
    数据库中的恶意字符批处理
  • 原文地址:https://www.cnblogs.com/yuluoxingkong/p/15076604.html
Copyright © 2020-2023  润新知