Java函数性能分析
测试函数性能,比较两个函数的执行效率差异是开发时经常面临的场景,像Go官方提供了 benchmark
工具,那么Java呢?
Java也提供了一款官方的微基准测试工具: JMH !
-
JMH 怎么使用?
-
引入
maven
依赖<dependency> <groupId>org.openjdk.jmh</groupId> <artifactId>jmh-core</artifactId> <version>1.23</version> </dependency> <dependency> <groupId>org.openjdk.jmh</groupId> <artifactId>jmh-generator-annprocess</artifactId> <version>1.23</version> <scope>provided</scope> </dependency>
函数添加注解:
@Benchmark
-
函数或所在类添加注解:
-
@BenchmarkMode
用来配置 Mode 选项-
Throughput
:整体吞吐量,每秒执行了多少次调用,单位为ops/time
-
AverageTime
:用的平均时间,每次操作的平均时间,单位为time/op
-
SampleTime
:随机取样,最后输出取样结果的分布 -
SingleShotTime
:只运行一次,往往同时把 Warmup 次数设为 0,用于测试冷启动时的性能 -
All
:上面的所有模式都执行一次
-
-
@Warmup
预热所需要配置的一些基本测试参数-
iterations
:预热的次数 -
time
:每次预热的时间 -
timeUnit
:时间的单位,默认秒 -
batchSize
:批处理大小,每次操作调用几次方法
-
@Measurement
实际调用方法所需要配置的一些基本测试参数@Threads
每个进程中的测试线程@Fork
进行 fork 的次数。如果 fork 数是 2 的话,则 JMH 会 fork 出两个进程来进行测试。-
@State
可以指定一个对象的作用范围。JMH 根据 scope 来进行实例化和共享操作。-
Scope.Benchmark
:所有测试线程共享一个实例,测试有状态实例在多线程共享下的性能 -
Scope.Group
:同一个线程在同一个 group 里共享实例 -
Scope.Thread
:默认的 State,每个测试线程分配一个实例
-
@OutputTimeUnit
为统计结果的时间单位@Param
指定某项参数的多种情况,特别适合用来测试一个函数在不同的参数输入的情况下的性能,只能作用在字段上,使用该注解必须定义 @State 注解。
-
-
-
简单的代码样例参考?
package org.example; import org.openjdk.jmh.annotations.*; import org.openjdk.jmh.runner.Runner; import org.openjdk.jmh.runner.RunnerException; import org.openjdk.jmh.runner.options.Options; import org.openjdk.jmh.runner.options.OptionsBuilder; import java.util.concurrent.TimeUnit; @BenchmarkMode(Mode.AverageTime) @State(Scope.Thread) @Fork(1) @OutputTimeUnit(TimeUnit.MILLISECONDS) @Warmup(iterations = 3) @Measurement(iterations = 5) public class JmhTest { String string = ""; StringBuilder stringBuilder = new StringBuilder(); @Benchmark public String stringAdd() { for (int i = 0; i < 1000; i++) { string = string + i; } return string; } @Benchmark public String stringBuilderAppend() { for (int i = 0; i < 1000; i++) { stringBuilder.append(i); } return stringBuilder.toString(); } public static void main(String[] args) throws RunnerException { Options opt = new OptionsBuilder() .include(JmhTest.class.getSimpleName()) .build(); new Runner(opt).run(); } }
使用JMH时需要注意
测试代码注意避免JVM的优化,导致JMH分析结果对你出现误判。JMH运行统计是基于JVM运行,你的代码在JVM运行前会被JIT优化。包括但不限于:
- 死码消除
@Benchmark public void testStringAdd(Blackhole blackhole) { String a = ""; for (int i = 0; i < length; i++) { a += i; } } // JVM 可能会认为变量 a 从来没有使用过,从而进行优化把整个方法内部代码移除掉,这就会影响测试结果。 // MH 提供了两种方式避免这种问题,一种是将这个变量作为方法返回值 return a,一种是通过 Blackhole 的 consume 来避免 JIT 的优化消除。
- 常量折叠
- 常量传播
- .....(更多的陷阱demo参考: https://github.com/lexburner/JMH-samples)