网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
复制代码
下面,我们逐一介绍一下比较关键的注解和参数。
@Warmup
样例。
@Warmup(
iterations = 5,
time = 1,
timeUnit = TimeUnit.SECONDS)
复制代码
我们不止一次提到预热,warmup
这个注解,可以用在类或者方法上,进行预热配置。可以看到,它有几个配置参数。
-
timeUnit
:时间的单位,默认的单位是秒。 -
iterations
:预热阶段的迭代数。 -
time
:每次预热的时间。 -
batchSize
:批处理大小,指定了每次操作调用几次方法。
上面的注解,意思是对代码预热总计5秒(迭代5次,每次一秒) 。预热过程的测试数据,是不记录测量结果的。
我们可以看一下它执行的效果:
Warmup: 3 iterations, 1 s each
Warmup Iteration 1: 0.281 ops/ns
Warmup Iteration 2: 0.376 ops/ns
Warmup Iteration 3: 0.483 ops/ns
复制代码
一般来说,基准测试都是针对的比较小的、执行速度相对较快的代码块。这些代码有很大的可能被编译、内联,在编码的时候保持方法的精简,对JIT也是有好的。
说到预热,就不得不提一下在分布式环境下的服务预热。在对服务节点进行发布的时候,通常也会有预热过程,逐步放量到相应的服务节点,直到服务达到最优状态。如下图所示,负载均衡负责这个放量过程,一般是根据百分比进行放量。
@Measurement
样例如下。
@Measurement(
iterations = 5,
time = 1,
timeUnit = TimeUnit.SECONDS)
复制代码
Measurement
和Warmup
的参数是一样的。不同于预热,它指的是真正的迭代次数。
我们能够从日志中看到这个执行过程:
Measurement: 5 iterations, 1 s each
Iteration 1: 1646.000 ns/op
Iteration 2: 1243.000 ns/op
Iteration 3: 1273.000 ns/op
Iteration 4: 1395.000 ns/op
Iteration 5: 1423.000 ns/op
复制代码
虽然经过预热之后,代码都能表现出它的最优状态,但一般和实际应用场景还是有些出入的。如果你的测试机器性能很高,或者你的测试机资源利用已经达到了极限,都会影响测试结果的数值。通常情况下,我都会在测试的时候,给机器充足的资源,保持一个稳定的环境。在分析结果的时候,也更加关注不同实现方式的性能差异,而不是测试数据本身。
@BenchmarkMode
此注解用来指定基准测试类型,对应Mode选项,用来修饰类和方法都可以。这里的value,是一个数组,可以配置多个统计维度。比如:
@BenchmarkMode({Throughput,Mode.AverageTime})
。统计的就是吞吐量和平均执行时间两个指标。
所谓的模式,在JMH中,可以分为以下几种:
-
Throughput: 整体吞吐量,比如QPS,单位时间内的调用量等。
-
AverageTime: 平均耗时,指的是每次执行的平均时间。如果这个值很小不好辨认,可以把统计的单位时间调小一点。
-
SampleTime: 随机
取样
。 -
SingleShotTime: 如果你想要测试仅仅一次的性能,比如第一次初始化花了多长时间,就可以使用这个参数,其实和传统的main方法没有什么区别。
-
All: 所有的指标,都算一遍,你可以设置成这个参数看下效果。
我们拿平均时间,看一下一个大体的执行结果:
Result “com.github.xjjdog.tuning.BenchmarkTest.shift”:
2.068 ±(99.9%) 0.038 ns/op [Average]
(min, avg, max) = (2.059, 2.068, 2.083), stdev = 0.010
CI (99.9%): [2.030, 2.106] (assumes normal distribution)
复制代码
由于我们声明的时间单位是纳秒,本次shift方法的平均响应时间就是2.068纳秒。
我们也可以看下最终的耗时时间。
Benchmark Mode Cnt Score Error Units
BenchmarkTest.div avgt 5 2.072 ± 0.053 ns/op
BenchmarkTest.shift avgt 5 2.068 ± 0.038 ns/op
复制代码
由于是平均数,这里的Error值的是误差的意思(或者波动)。
可以看到,在衡量这些指标的时候,都有一个时间维度,它就是通过**@OutputTimeUnit**注解进行配置的。
这个就比较简单了,它指明了基准测试结果的时间类型。可用于类或者方法上。一般选择秒、毫秒、微秒,纳秒那是针对的速度非常快的方法。
举个例子,@BenchmarkMode(Mode.Throughput)
和@OutputTimeUnit(TimeUnit.MILLISECONDS)
进行组合,代表的就是每毫秒的吞吐量。
如下面的关于吞吐量的结果,就是以毫秒计算的。
Benchmark Mode Cnt Score Error Units
BenchmarkTest.div thrpt 5 482999.685 ± 6415.832 ops/ms
BenchmarkTest.shift thrpt 5 480599.263 ± 20752.609 ops/ms
复制代码
OutputTimeUnit
注解同样可以修饰类或者方法,通过更改时间级别,可以获取更加易读的结果。
@Fork
fork的值一般设置成1,表示只使用一个进程进行测试;如果这个数字大于1,表示会启用新的进程进行测试;但如果设置成0,程序依然会运行,不过这样是在用户的JVM进程上运行的,可以看下下面的提示,但不推荐这么做。
Fork: N/A, test runs in the host VM
*** WARNING: Non-forked runs may silently omit JVM options, mess up profilers, disable compiler hints, etc. ***
*** WARNING: Use non-forked runs only for debugging purposes, not for actual performance runs. ***
复制代码
那么fork到底是在进程还是线程环境里运行呢?我们追踪一下JMH的源码,发现每个fork进程是单独运行在Proccess
进程里的,这样就可以做完全的环境隔离,避免交叉影响。它的输入输出流,通过Socket连接的模式,发送到我们的执行终端。
在这里分享一个小技巧。其实fork注解有一个参数叫做jvmArgsAppend
,我们可以通过它传递一些JVM的参数。
@Fork(value = 3, jvmArgsAppend = {“-Xmx2048m”, “-server”, “-XX:+AggressiveOpts”})
复制代码
在平常的测试中,也可以适当增加fork数,来减少测试的误差。
@Threads
fork
是面向进程的,而Threads
是面向线程的。指定了这个注解以后,将会开启并行测试。
如果配置了 Threads.MAX ,则使用和处理机器核数相同的线程数。
@Group
@Group注解只能加在方法上,用来把测试方法进行归类。如果你单个测试文件中方法比较多,或者需要将其归类,则可以使用这个注解。
与之关联的@GroupThreads
注解,会在这个归类的基础上,再进行一些线程方面的设置。
@State
@State 指定了在类中变量的作用范围。它有三个取值。
@State 用于声明某个类是一个“状态”,可以用Scope 参数用来表示该状态的共享范围。这个注解必须加在类上,否则提示无法运行。
Scope有如下3种值:
-
Benchmark:表示变量的作用范围是某个基准测试类。
-
Thread:每个线程一份副本,如果配置了Threads注解,则每个Thread都拥有一份变量,它们互不影响。
-
Group:联系上面的@Group注解,在同一个Group里,将会共享同一个变量实例。
在JMHSample04DefaultState
测试文件中,演示了变量x
的默认作用范围是Thread,关键代码如下:
@State(Scope.Thread)
public class JMHSample_04_DefaultState {
double x = Math.PI;
@Benchmark
public void measure() {
x++;
}
}
复制代码
@Setup和@TearDown
和单元测试框架JUnit类似,用于基准测试前的初始化动作, @TearDown 用于基准测试后的动作,来做一些全局的配置。
这两个注解,同样有一个Level值,标明了方法运行的时机,它有三个取值。
-
Trial:默认的级别。也就是Benchmark级别。
-
Iteration:每次迭代都会运行。
-
Invocation:每次方法调用都会运行,这个是粒度最细的。
@Param
@Param 注解只能修饰字段,用来测试不同的参数,对程序性能的影响。配合@State注解,可以同时制定这些参数的执行范围。
代码样例如下:
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(1)
@State(Scope.Benchmark)
public class JMHSample_27_Params {
@Param({“1”, “31”, “65”, “101”, “103”})
public int arg;
@Param({“0”, “1”, “2”, “4”, “8”, “16”, “32”})
public int certainty;
@Benchmark
public boolean bench() {
return BigInteger.valueOf(arg).isProbablePrime(certainty);
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JMHSample_27_Params.class.getSimpleName())
// .param(“arg”, “41”, “42”) // Use this to selectively constrain/override parameters
.build();
new Runner(opt).run();
}
}
复制代码
值得注意的是,如果你设置了非常多的参数,这些参数将执行多次,通常会运行很长时间。比如参数1 M个,参数2 N个,那么总共要执行M*N次。
下面是一个执行结果的截图。
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
opt).run();
}
}
复制代码
值得注意的是,如果你设置了非常多的参数,这些参数将执行多次,通常会运行很长时间。比如参数1 M个,参数2 N个,那么总共要执行M*N次。
下面是一个执行结果的截图。
[外链图片转存中…(img-DfaanEmv-1715404913604)]
[外链图片转存中…(img-wj3zOPgG-1715404913605)]
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!