看开源项目时,时不常遇到一个叫做benchmark的目录,此时脑子停滞,一眼带过,最近一次看到就顺手问了下谷大哥,发现benchmark还是个挺有意思的东西。
基准测试是什么
基准测试
是指通过设计科学的测试方法、测试工具和测试系统,实现对一类测试对象的某项性能指标进行定量的和可对比的测试。
例如,对计算机CPU进行浮点运算、数据访问的带宽和延迟等指标的基准测试
,可以使用户清楚地了解每一款CPU的运算性能及作业吞吐能力是否满足应用程序的要求;再如对数据库管理系统的ACID(Atomicity, Consistency, Isolation, Durability, 原子性、一致性、独立性和持久性)、查询时间和联机事务处理能力等方面的性能指标进行基准测试
,也有助于使用者挑选最符合自己需求的数据库系统。
通过基准测试
我们可以了解某个软件在给定环境下的性能表现,对使用者而言可以用作选型的参考,对开发者而言可以作为后续改进的基本参照。
JMH是什么
JMH(Java Microbenchmark Harness)是Java用来做基准测试一个工具,该工具由openJDK提供并维护,测试结果可信度较高,该项目官方还在持续更新中。
下面只是JMH简单描述,正所谓“纸上得来终觉浅,绝知此事要躬行”,要想全面了解还得读完官方给出的Demo或者看我的翻译注解版本的官方Demo。
已经给标题加链接,直接戳标题即可闪现到例子。官方例子有37个,这里只列出了梗概。
举个例子
功能入门第一课,Hello World!
1 | public class JMHSample_01_HelloWorld { |
看下输出:
1 | # JMH version: 1.21 |
上面的报告显示,我们用1个线程对吞吐量测量,测量和预热分别迭代了5次,最终得出的Score(ops/s)是2962827230.624 ± 171803440.922,测量过程中没有出现Error。
测试控制
测试输出结果中开头的配置项都是我们可以通过注解、编程或者命令行的方式来控制的。它可以具体到每个benchmark。
@Fork
需要运行的试验(迭代集合)数量。每个试验运行在单独的JVM进程中。也可以指定(额外的)JVM参数。@Measurement
提供真正的测试阶段参数。指定迭代的次数,每次迭代的运行时间和每次迭代测试调用的数量(通常使用@BenchmarkMode(Mode.SingleShotTime)
测试一组操作的开销——而不使用循环)@Warmup
与@Measurement
相同,但是用于预热阶段@Threads
该测试使用的线程数。默认是Runtime.getRuntime().availableProcessors()
测试维度
通过JMH提供的工具我们可以轻松的的获得某个功能的吞吐量、平均运行时间、冷启动等指标的数据。
这些输出结果通过@BenchmarkMode
注解来控制,它的值定义在枚举类org.openjdk.jmh.annotations.Mode
中。
@BenchmarkMode
接受的参数是一个Mode
数据,也就是说,我们可以指定一个或者多个Mode
,测试时会把我们指定的模式依次运行,打印出结果。
1 | public enum Mode { |
数据的共享
测试的时候,我们可能需要向测试方法传入若干参数,这些参数还可能需要不同的隔离级别:每个线程单独一份还是每个benchmark一份还是一组线程共享等。
这些参数有以下要求:
- 有无参构造函数(默认构造函数)
- 是公共类
- 内部类应该是静态的
- 该类必须使用
@State
注解
参数必须是对象,因为@State
被定义为ElementType.TYPE
,即:可以用来注解类、接口或者枚举。
@State
接受单个配置值Scope
。
1 | public enum Scope { |
数据的准备
数据的准备像极了JUnit和TestNG的方式,即:在测试开始前后分别进行处理。在JMH中对应到@Setup
和@TearDown
,我们可以在被进行标记的方法中对数据进行处理,并且处理的耗时不被记入正常测试的时间,也就是说不会影响我们测试结果。
@Setup
和@TearDown
分别接受单个配置值Level
。
1 | public enum Level { |
测试线程组
我们可以通过指定线程组的的方式来模拟一些场景,比如:生产-消费。这种场景下生产消费的线程数量往往是不一致的,通过@Group
和@GroupThreads
我们可以很轻松的制造出这种场景。
具体的使用说明例子中说的很详细 —-传送门—-> JMHSample_15_Asymmetric
编译器的控制
我们知道编译器在编译时会做一些优化,这个例子展示了如何用@CompilerControl
来控制编译对方法内敛优化的控制。
1 | public CompilerControl { |
基准测试中建议
编译器还有其他一些优化行为,常见的包括(更多参见):死代码消除(DCE)、方法内敛(method inline)、循环优化、常量折叠。
方法内敛可以用上面提到的注解做控制,但更多的需要我们在些测试时采用一些小技巧规避,比如:DCE可以根据情况采用Blackhole消费输出结果。
测试前后较重的处理放在
@Setup
和@TearDown
中注意DCE(死代码消除)
不出现循环被编译器优化,具体建议参考JMHSample_34_SafeLooping
测试必须为fork,fork是分离出来子进程进行测试,
@fork(2)
含义为顺次(one-by-one)fork出子进程来测试使用
@fork
多次fork测试,减少运行间差异多线程测试时参考JMHSample_17_SyncIterations
对非循环方法需要测量冷启动的时间消耗,参考JMHSample_26_BatchSize
可以通过profiler获得基准测试时JVM的相关信息,比如栈、gc、classloader。参考JMHSample_35_Profilers
在使用profiler时遇到
no tty present and no askpass program specified
错误是因为帐号并没有开启sudo免密导致的。
通过以下步骤可解决,但测试完成后安全起见建议删除:sudo visudo;在文件最后追加 userName ALL=(ALL) NOPASSWD:ALL
知识点记录
编译优化
常见的编译器优化包括(更多参见):死代码消除(DCE)、方法内敛(method inline)、循环优化、常量折叠。
方法内敛
许多优化手段都试图消除机器级跳转指令(例如,x86架构的JMP指令)。跳转指令会修改指令指针寄存器,因此而改变了执行流程。
相比于其他汇编指令,跳转指令是一个代价高昂的指令,这也是为什么大多数优化手段会试图减少甚至是消除跳转指令。
内联是一种家喻户晓而且好评如潮的优化手段,这是因为跳转指令代价高昂,而内联技术可以将经常调用的、具有不容入口地址的小方法整合到调用方法中。
Listing 3到Listing 5中的Java代码展示了使用内联的用法。
Listing 3. Caller method1
2
3int whenToEvaluateZing(int y) {
return daysLeft(y) + daysLeft(0) + daysLeft(y+1);
}
Listing 4. Called method1
2
3
4
5
6int daysLeft(int x){
if (x == 0)
return 0;
else
return x - 1;
}
Listing 5. Inlined method1
2
3
4
5
6
7
8
9int whenToEvaluateZing(int y){
int temp = 0;
if(y == 0) temp += 0; else temp += y - 1;
if(0 == 0) temp += 0; else temp += 0 - 1;
if(y+1 == 0) temp += 0; else temp += (y + 1) - 1;
return temp;
}
在Listing 3到Listing 5的代码中,展示了将调用3次小方法进行内联的示例,这里我们认为使用内联比跳转有更多的优势。
如果被内联的方法本身就很少被调用的话,那么使用内联也没什么意义,但是对频繁调用的“热点”方法进行内联在性能上会有很大的提升。
此外,经过内联处理后,就可以对内联后的代码进行进一步的优化,正如Listing 6中所展示的那样。
Listing 6. After inlining, more optimizations can be applied1
2
3
4
5int whenToEvaluateZing(int y){
if(y == 0) return y;
else if (y == -1) return y - 1;
else return y + y - 1;
}
unrolled loop 循环展开
1 | for (i = 1; i <= 60; i++) |
可以如此循环展开:
1 | for (i = 1; i <= 20; i+=3) |
分支预测
底层对循环中判断的优化。简单理解,对执行的循环判断做采样,根据采样来预测下一个判断会走到哪个分支中。
参考这篇文章—->深入理解CPU的分支预测(Branch Prediction)模型<—-做进一步了解。
小生不才,以上如有描述有误的地方还望各位不吝赐教 !^_^!