在以前,想要知道自己写的程序性能和程序运行时间,一般都是定义一个startTime=System.nanoTime
,再定义一个endTime=System.nanoTime
,最后他们之间的差便是程序的运行时间。
但是,这样的测试仅仅是粗略的测试此段代码在此时的运行时间,结果并不一定可靠。这里建议使用一个更加官方的测试工具:JMH–Java微基准测试框架
Code Tools: jmh
JMH是由OpenJDK官方发布的一种Java工具,用于构建,运行和分析用Java和其他语言编写的针对JVM的nano / micro / milli / macro基准测试。
官方地址:https://openjdk.java.net/projects/code-tools/jmh/
使用JMH,你可以:
- 当需要优化一个方法的时候,可以使用JMH测试方法运行时间,测试是否达到优化效果
- 当需要知道一个方法大概的执行时间的时候
- 当需要知道不同的参数对同一个方法的运行效率的影响的时候
总之,JMH在方法级别的运行时间测试上得到的结果是比较权威的。
Go Start
官方推荐JMH应该使用Maven构建。
- 当我们仅仅需要测试一个独立的方法的时候,我们可以直接构建一个JMH Maven骨架(archetype)
mvn archetype:generate \
-DinteractiveMode = false \
-DarchetypeGroupId = org.openjdk.jmh \
-DarchetypeArtifactId = jmh- java -benchmark-archetype \
-DgroupId = org.sample \
-DartifactId = test \
-Dversion = 1.0
- 当我们想在已有到的项目的基础上进行JMH测试的时候,我们可以直接添加Maven依赖:
<!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-core --> <dependency> <groupId>org.openjdk.jmh</groupId> <artifactId>jmh-core</artifactId> <version>{jmh.version}</version> </dependency> <!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-generator-annprocess --> <dependency> <groupId>org.openjdk.jmh</groupId> <artifactId>jmh-generator-annprocess</artifactId> <version>1.21</version> <scope>{jmh.version}</scope> </dependency>
添加完依赖后,我们就可以编写性能测试方法,这里和我们经常使用的单元测试方法差不多:
这里我们测试下HashMap中,使用位运算代替取模运行的效率到底有没有提升
@BenchmarkMode(Mode.Throughput)
@Warmup(iterations = 3)
@Measurement(iterations = 10, time = 5, timeUnit = TimeUnit.SECONDS)
@Threads(8)
@Fork(2)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
public class Test {
@Param({"18","2035","12345"})
private int param;
@Benchmark
public void test(){
int c=(16-1)¶m;
}
@Benchmark
public void test2(){
int c=param%16;
}
@Setup
public void prepare() {
}
}
执行:
执行方式分两种
- 由于JMH测试一般测试好几轮取平均值,因此有时候测试时间比较长,这个时候我们可以使用Maven将测试打包,然后放在服务器上测试:
mvn clean package
然后会在对应的目录/target下生成jar包,直接
java -jar xxx.jar
运行即可。
-
对于比较小的项目,我们可以直接编写Main方法,在编译器上运行即可:
public static void main(String[] args)throws Exception { Options options = new OptionsBuilder() .include(Test.class.getSimpleName()) .output("G:/Benchmark.log") .build(); new Runner(options).run(); }
这里我们指定了测试类和日志输出路径。
执行完成后可以获取测试结果:
# JMH version: 1.21 # VM version: JDK 1.8.0_181, Java HotSpot(TM) 64-Bit Server VM, 25.181-b13 # VM invoker: H:\java\jdk1.8\jre\bin\java.exe # VM options: -Dvisualvm.id=789216946682180 -javaagent:H:\idea\IntelliJ IDEA 2018.2.2_2\lib\idea_rt.jar=64857:H:\idea\IntelliJ IDEA 2018.2.2_2\bin -Dfile.encoding=UTF-8 # Warmup: 3 iterations, 10 s each # Measurement: 10 iterations, 5 s each # Timeout: 10 min per iteration # Threads: 8 threads, will synchronize iterations # Benchmark mode: Throughput, ops/time # Benchmark: com.dengchengchao.Test.test # Parameters: (param = 18) ... ... ... Benchmark (param) Mode Cnt Score Error Units Test.test 18 thrpt 20 11201407.357 ± 288038.173 ops/ms Test.test 2035 thrpt 20 11155849.134 ± 411417.605 ops/ms Test.test 12345 thrpt 20 11242387.836 ± 124937.944 ops/ms Test.test2 18 thrpt 20 10618259.409 ± 166619.563 ops/ms Test.test2 2035 thrpt 20 10566613.381 ± 185395.887 ops/ms Test.test2 12345 thrpt 20 10497633.170 ± 226031.760 ops/ms
可以发现,结果中说明了每个方法的平均吞吐量以及误差范围等,我们可以得到结果:在我的电脑中,JDK 1.8环境下,使用Java 的位运算和取模运行时间相差不大,
在这里,使用JMH测试方法运行就算完成了,其实也比较简单,并且比自己写System.nanoTime
更加准确。
注解介绍
在上面的方法测试中,我们发现了很多新的注解。下面对上这些注解进行简单介绍:
@Benchmark(测试方法标记注解)
标记测试方法,和@Test注解差不多,标注后在运行的时候JMH将会对此 方法进行测试。
在官方的HelloWorld例子仅仅使用了这一个注解,因为这里的注解其实也是一种启动参数,不仅仅可以通过注解表示,还可以使用类似
java -jar xxx.jar -f 1 -t 2
方式指定,也可以在编写Main方法的时候,使在构建方法Builder的时候指定。这种灵活的参数指定方式,可以在不同的场景的灵活应用。
@BenchmarkMode(基准测试类型)
- Throughput : 吞吐量,比如:1s内可以执行多少次调用
- AverageTime: 调用的平均时间
- SampleTIme: 随机取样,最后输出取样结果分布,例如“99%的调用在xxx毫秒以内,99.99%的调用在xxx毫秒以内”
- SingleShotTime:以上模式都是默认一次 iteration 是 1s,唯有 SingleShotTime 是只运行一次。往往同时把 warmup 次数设为0,用于测试冷启动时的性能。
- All:执行所有的类型测试
@Measurement(基本测试参数)
- iterations 进行测试的轮次
-
time 每轮进行的时长
-
timeUnit 时长单位
@Warmup
基准测试预热选项,有时候在JVM的优化下(比如JIT),前几次测试的结果一般不准确,因此我们可以指定预热时间,将前几次的测试结果丢弃。@Warmup
的选项和@Measurement
是相同的
@Threads
每个进程中的测试线程,根据具体情况选择,一般为cpu乘以2。
@Fork
启动测试进程数,比如@Fork(2),那么JMH会启动两个进程进行测试
@OutputTimeUnit
基准测试时间单位,比如秒,毫秒,微秒等
@Param
指定参数,可以用来测试在不同参数的情况下方法的运行效率
@Setup
方法级注解,被标记的方法会在启动前运行,一般用来在测试之前进行一些准备工作,比如数据初始化等
@TearDown
方法级注解,被标记的方法会在测试完成后运行,一般用来在测试之后的一些清理工作,比如关闭连接等
@State
当使用@Setup参数的时候,必须在类上加这个参数,不然会提示无法运行。
State 用于声明某个类是一个“状态”,然后接受一个 Scope 参数用来表示该状态的共享范围。 因为很多 benchmark 会需要一些表示状态的类,JMH 允许你把这些类以依赖注入的方式注入到 benchmark 函数里。Scope 主要分为三种。
- Thread: 该状态为每个线程独享。
-
Group: 该状态为同一个组里面所有线程共享。
-
Benchmark: 该状态在所有线程间共享。
官方文档中,有个比较好的例子来展示@state的使用:JMHSample_03_States
在JMH能做的,远不止这些,想要了解更多,可以阅读官方文档
参考文章: