JMH如何使用
JMH如何使用
JMH的基本用法
基础注解 @Benchmark
在一个基本测试类中至少
包含一个被@Benchmark
标记的方法,否则会抛出异常。
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Thread)
public class JMHExample02 {
public void normalMethod() {
}
public static void main(String[] args) throws RunnerException {
final Options opts = new OptionsBuilder().include(JMHExample02.class.getSimpleName())
.forks(1)
.measurementIterations(10)
.warmupIterations(10)
.build();
new Runner(opts).run();
}
}
异常:Exception in thread "main" No benchmarks to run; check the include/exclude regexps.
Warmup以及Measurement
Warmup: "热身",使得在度量之前,类经历了早期的优化、JVM运行期编译、JIT优化
Measurement: 真正的度量操作,度量过程中的所有数据都会被纳入统计
全局设置
-
构造Options时设置批次执行
final Options opts = new OptionsBuilder().include(JMHExample02.class.getSimpleName()) .forks(1) //度量执行批次为5 .measurementIterations(5) //在度量之前先执行两次热身 .warmupIterations(2) .build(); new Runner(opts).run();
-
@Warmup与@Measurement注解
@BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.MICROSECONDS) @State(Scope.Thread) @Measurement(iterations = 5) @Warmup(iterations = 2) public class JMHExample02
局部设置(基准测试方法之上)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Thread)
@Measurement(iterations = 5)
@Warmup(iterations = 3)
public class JMHExample03 {
@Benchmark
public void test1() throws InterruptedException {
TimeUnit.MILLISECONDS.sleep(10);
}
@Benchmark
@Warmup(iterations = 5)
@Measurement(iterations = 4)
public void test2() throws InterruptedException {
TimeUnit.MILLISECONDS.sleep(1);
}
public static void main(String[] args) throws RunnerException {
final Options opts = new OptionsBuilder().include(JMHExample03.class.getSimpleName())
.forks(1)
//.measurementIterations(2)
//.warmupIterations(2)
.build();
new Runner(opts).run();
}
}
执行结果:
基准测试方法 | 测试结果 |
---|---|
test1 | 预热3次、度量5次 |
test2 | 预热5次、度量4次 |
Warmup以及Measurement在基准测试方法上的设置会覆盖全局设置,但是无法覆盖Options中构建的全局设置
BenchmarkMode
四种模式概念
模式 | 使用 |
---|---|
AverageTime | 平均响应时间(方法每一次调用) |
Throughput | 单位时间内方法的调用次数 |
SampleTime | 抽样的方式统计性能数据 |
SingleShotTime | 无论warmup还是measurement,每批次中,基准测试方法只会执行一次(warmup一般设置为0) |
多模式设置
我们可以在基准测试方法上设置多个模式,甚至是全部
@BenchmarkMode({Mode.AverageTime,Mode.Throughput})
@Benchmark
public void testThroughputAndAverageTime(){
TimeUnit.MILLISECONDS.sleep(1);
}
@BenchmarkMode(Mode.All)
@Benchmark
public void testAll(){
TimeUnit.MILLISECONDS.sleep(1);
}
覆盖次序:基准方法上的设置会覆盖类上的设置,Options上的设置会覆盖所有的设置。
OutputTimeUnit
提供了统计结果输出时的时间单位,覆盖次序与BenchmarkMode一致
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Thread)
@Measurement(iterations = 5)
@Warmup(iterations = 3)
public class JMHExample04 {
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Benchmark
public void test() throws InterruptedException {
TimeUnit.SECONDS.sleep(1);
}
public static void main(String[] args) throws RunnerException {
final Options opts = new OptionsBuilder().include(JMHExample04.class.getSimpleName())
.include(JMHExample04.class.getSimpleName())
.timeUnit(TimeUnit.NANOSECONDS)
.forks(1)
.build();
new Runner(opts).run();
}
}
三种State的使用
Thread独享(共享)的State
- Scope.Thread: 每个运行基准测试方法的线程都拥有一个独立的对象实例
- Scope.Benchmark: 多个线程共享同一个对象实例
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Fork(1)
@Measurement(iterations = 5)
@Warmup(iterations = 3)
//设置5个线程运行基准测试方法
@Threads(5)
public class JMHExample07 {
@State(Scope.Benchmark)
//@State(Scope.Benchmark)
public static class Test {
public Test() {
System.out.println("create instance");
}
public void method() {
}
}
@Benchmark
public void test(Test test){
test.method();
}
public static void main(String[] args) throws RunnerException {
final Options opts = new OptionsBuilder()
.include(JMHExample07.class.getSimpleName())
.build();
new Runner(opts).run();
}
}
执行结果:
@State(Scope.Thread
)输出了5次 "create instance"
@State( Scope.Benchmark
)只输出了1次 "create instance"
上述的“对象”是指被@State标记的类实例。可通过基准测试方法的参数引入(如上),或是直接运行基准测试方法所在的宿主class。
@Thread注解设置参与基准测试的线程数
线程组共享的State
- Scope.Group
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Fork(1)
@Measurement(iterations = 5)
@Warmup(iterations = 3)
//设置5个线程运行基准测试方法
@Threads(5)
public class JMHExample08 {
@State(Scope.Group)
public static class Test {
public Test() {
System.out.println("create instance");
}
public void read() {
System.out.println("test read");
}
public void write() {
System.out.println("test write");
}
}
@GroupThreads(3)
@Group("test")
@Benchmark
public void testRead(Test test){
test.read();
}
@GroupThreads(3)
@Group("test")
@Benchmark
public void testWrite(Test test){
test.write();
}
public static void main(String[] args) throws RunnerException {
final Options opts = new OptionsBuilder()
.include(JMHExample08.class.getSimpleName())
.build();
new Runner(opts).run();
}
}
执行结果:testRead()与testWrite()交替执行
前两种
State
的情况下,基准测试方法都只能按照顺序逐个执行。而想要多个方法并行地去访问共享数据,则需要Scope.Group
@Param的使用
假如我们现在想要对两个不同类型的Map进行微基准的性能测试,该怎么做呢?按照前面的方法,我们可以为两个Map分别编写微基准测试方法,如下:
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Fork(1)
@Measurement(iterations = 5)
@Warmup(iterations = 3)
//设置5个线程运行基准测试方法
@Threads(5)
@State(Scope.Benchmark)
public class JMHExample09 {
private Map<Long, Long> concurrentHashMap;
private Map<Long, Long> synchronizedMap;
@Setup
public void setUp(){
concurrentHashMap = new ConcurrentHashMap<>();
synchronizedMap = Collections.synchronizedMap(new HashMap<Long, Long>());
}
@Benchmark
public void testConcurrentHashMap(){
this.concurrentHashMap.put(System.nanoTime(),System.nanoTime());
}
@Benchmark
public void testSynchronizedMap(){
this.concurrentHashMap.put(System.nanoTime(),System.nanoTime());
}
public static void main(String[] args) throws RunnerException {
final Options opts = new OptionsBuilder()
.include(JMHExample09.class.getSimpleName())
.build();
new Runner(opts).run();
}
}
但是当我们想对更多类型的集合(或是其他的东东)进行微基准测试时,这种方法显然就多了很多的冗余代码,此时我们就可以使用@Param来简化代码啦,如下:
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Fork(1)
@Measurement(iterations = 5)
@Warmup(iterations = 3)
//设置5个线程运行基准测试方法
@Threads(5)
//多个线程共享实例
@State(Scope.Benchmark)
public class JMHExample10 {
@Param({"1","2","3","4"})
private int type;
Map<Object, Object> map = null;
@Setup
public void setUp(){
switch (type){
case 1:
this.map = new ConcurrentHashMap<>();
break;
case 2:
this.map = new ConcurrentSkipListMap<>();
break;
case 3:
this.map = new Hashtable<>();
break;
case 4:
this.map = Collections.synchronizedMap(new HashMap<>());
}
}
@Benchmark
public void test(){
this.map.put(System.nanoTime(),System.nanoTime());
}
public static void main(String[] args) throws RunnerException {
final Options opts = new OptionsBuilder()
.include(JMHExample10.class.getSimpleName())
.build();
new Runner(opts).run();
}
}
执行结果:
结果中只截取了部分关键输出,多出的type列正是对应@Param所提供的参数
Benchmark (type) Mode Cnt Score Error Units
JMHExample09.testConcurrentHashMap 1 avgt 5 23098.216 ± 161361.562 us/op
JMHExample09.testConcurrentHashMap 2 avgt 5 45467.394 ± 103183.828 us/op
JMHExample09.testConcurrentHashMap 3 avgt 5 61373.892 ± 243766.954 us/op
JMHExample09.testConcurrentHashMap 4 avgt 4 140614.207 ± 650830.876 us/op
各个字段含义:
Mode | Cnt | Score | Error | Units | type |
---|---|---|---|---|---|
模式(四种) | 基准方法调用次数 | 响应时间 | 时间偏差 | 时间单位 | @Param参数 |
有了
@Param
之后,我们只需要编写一次微基准测试方法即可,JMH会根据@Param提供的参数值自动执行基准测试以及统计。
JMH的测试套件
Setup以及TearDown
@Setup
: 基准测试方法之前调用,通常用于资源的初始化。@TearDown
: 基准测试方法之后调用,通常用于资源的回收清理工作。
@BenchmarkMode(Mode.SingleShotTime)
@Fork(1)
@Warmup(iterations = 5)
@Measurement(iterations = 10)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Thread)
public class JMHExample11 {
private List<String> list = null;
@Setup
public void setUp(){
this.list = new ArrayList<>();
System.out.println("setUp...");
}
@Benchmark
public void testRight() {
this.list.add("Test");
}
@Benchmark
public void testWrong() {
//do nothing here
}
@TearDown
public void tearDown() {
System.out.println("tearDown...");
assert this.list.size() > 0 : "The Size Of List Must Lager Than Zero";
}
public static void main(String[] args) throws RunnerException {
final Options opts = new OptionsBuilder()
.include(JMHExample11.class.getSimpleName())
.jvmArgs("-ea") //enable assertion 激活断言
.build();
new Runner(opts).run();
}
}
执行结果:
# Warmup Iteration 1: setUp...
4.900 us/op
# Warmup Iteration 2: 0.600 us/op
# Warmup Iteration 3: 0.600 us/op
# Warmup Iteration 4: 0.400 us/op
# Warmup Iteration 5: 0.500 us/op
Iteration 1: 0.600 us/op
Iteration 2: 0.900 us/op
Iteration 3: 0.600 us/op
Iteration 4: 0.400 us/op
Iteration 5: 0.500 us/op
Iteration 6: 0.400 us/op
Iteration 7: 0.300 us/op
Iteration 8: 0.500 us/op
Iteration 9: 0.400 us/op
Iteration 10: tearDown...
使用Level控制测试套件
通过上面的结果可以知道,默认情况下,@Setup
与 @TearDown
的套件方法分别在所有执行批次之前与之后执行。JMH还提供了另外两种配置。
-
Trial:默认配置。@Setup(Level.Trial)
-
Iteration:因为可以设置Warmup与Measurement,所以基准方法可能被执行若干个批次。Iteration将允许我们在每个批次前后执行套件方法。
@Setup(Level.Iteration) public void setUp()
-
Invocation:每个批次(Warmup或Measurement)中都可能存在多次基准方法的调用,Invocation将允许我们在每一次调用基准方法的前后执行套件。
@Setup(Level.Invocation) public void setUp()
具体代码不做展示。