性能优化之代码的指令层优化
前两天发了一个关于三目运算符优化的动态,以code review的形式和大家交流,后来发现大家交流的很热烈,各抒己见,很nice,有沟通才有碰撞,有碰撞才有深刻的理解和进步。本文给出优化的原因和方法,感兴趣的可以看下。
讨论的内容如下,即下面划线部分的代码可以进行优化,理由是:已装箱的值被拆箱,然后又立即重新装箱!这对性能有影响
大家的讨论主要集中在两点:
- 怎么优化
- 没必要,纯属工作不饱和或者瞎折腾
针对第一点,今天本文会给出详细的答案;针对第二点,大家说的也没错,这种优化,你不管系统基本照样运行,但是,作为程序员,拿追剧、刷视频的时间来折腾一些技术,我是很喜欢的,喜欢和这种细节battle,不弄明白吃饭也不香。很多时候,我们要具有瞎折腾的勇气和好奇心,尤其是在当下这种浮躁的环境下;当然,这都是本文的题外话,扯远了。
OK,我们言归正传。为什么三目运算符中包装类型和基本类型共存时会影响性能呢?比如:
Integer brandId = Objects.isNull(content.getBrandId()) ? 0 : content.getBrandId();
首先影响性能是肯定的(性能优化不是银弹,高并发场景下我们需要方方面面的优化)。要知道上述代码为什么会影响性能就要从java代码的执行原理来说了,大家应该都知道我们写的.java文件要想被执行,需要经过加载->链接->初始化,然后JVM才会执行对应的代码,JVM拿到class文件即字节码文件去执行对应的指令。如下图:
简要流程.png
所以对于上图中的代码,即如下代码:
for (StrategyContentDO content : saveRequestDTO.getStrategyContentList()) {
Integer categoryId = content.getCategoryId();
Integer brandId = Objects.isNull(content.getBrandId()) ? 0 : content.getBrandId();
doCheck4Busi(uniqueChecker, content, categoryId, brandId);
}
我们需要拿到它的字节码对应的JVM执行指令,一看便知。你可以通过javac命令编译Java文件为字节码文件,即javac xxx.java,因为字节码文件我们人类看不懂,所以你可以再用javap命令将字节码文件反汇编为JVM的执行指令,这个执行指令我们是可以阅读的,即javap -c xxx。上述获取执行指令的过程觉得麻烦,这里推荐idea中的一个插件:jclasslib Bytecode Viewer(文末有使用说明,很简单很方便)。这里我贴出相关代码的执行指令,我们一起看下:
没有优化时的代码对应的JVM执行指令:
......省略一些无关指令
190 aload 6
192 invokevirtual #25 <com/demo/StrategyContentDO.getCategoryId : ()Ljava/lang/Integer;>
195 astore 7
197 aload 6
199 invokevirtual #26 <com/demo/StrategyContentDO.getBrandId : ()Ljava/lang/Integer;>
202 invokestatic #27 <java/util/Objects.isNull : (Ljava/lang/Object;)Z>
205 ifeq 212 (+7)
208 iconst_0
209 goto 220 (+11)
212 aload 6
214 invokevirtual #26 <com/demo/StrategyContentDO.getBrandId : ()Ljava/lang/Integer;>
217 invokevirtual #28 <java/lang/Integer.intValue : ()I>
220 invokestatic #4 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
223 astore 8
225 aload 4
227 aload 6
229 aload 7
231 aload 8
233 invokestatic #29 <com/demo/Foo.doCheck4Busi : (Ljava/util/Set;Lcom/demo/StrategyContentDO;Ljava/lang/Integer;Ljava/lang/Integer;)V>
236 goto 168 (-68)
239 return
从上述指令集中可以清晰的看到代码的执行过程:
JVM执行指令的流程.png
从代码的运行指令中我们看到,这段代码:
Integer brandId = Objects.isNull(content.getBrandId()) ? 0 : content.getBrandId();
当字段brandId等于null时,基本类型0需要装箱,不为null时,代码的执行要先拆箱再装箱。
那怎么优化呢?也很简单,我们保持三目运算符中字段类型一致即可(避免频繁装拆箱),代码处理如下:
//缓存的Integer常量
public static final Integer INTEGER_ZERO = 0;
三目运算符中保持类型一致.png
优化后Java代码对应的JVM执行指令集:
......省略一些无关的指令
190 aload 6
192 invokevirtual #25 <com/demo/StrategyContentDO.getCategoryId : ()Ljava/lang/Integer;>
195 astore 7
197 aload 6
199 invokevirtual #26 <com/demo/StrategyContentDO.getBrandId : ()Ljava/lang/Integer;>
202 invokestatic #27 <java/util/Objects.isNull : (Ljava/lang/Object;)Z>
205 ifeq 214 (+9)
208 getstatic #28 <com/demo/Foo.INTEGER_ZERO : Ljava/lang/Integer;>
211 goto 219 (+8)
214 aload 6
216 invokevirtual #26 <com/demo/StrategyContentDO.getBrandId : ()Ljava/lang/Integer;>
219 astore 8
221 aload 4
223 aload 6
225 aload 7
227 aload 8
229 invokestatic #29 <com/demo/Foo.doCheck4Busi : (Ljava/util/Set;Lcom/demo/StrategyContentDO;Ljava/lang/Integer;Ljava/lang/Integer;)V>
232 goto 168 (-64)
235 return
指令中没有装箱拆箱指令了.png
从优化后的指令中可以看出,三目运算符没有装拆箱的指令了,减少了JVM要执行的指令数,也就减小了系统的执行时间,这实际上就是所谓的性能优化中的指令层优化。
后记
性能优化不是银弹,这世界上不存在一种万能的数据结构去高性能的存、取任何类型的数据,也不存在一种万能的性能优化方法去优化所有的性能问题。缓存、接口内部处理并发化,外部调用异步化、合理的数据结构、合理的代码处理(预热、空间换时间等,而不是全部甩给JVM)等等等等,都是影响性能的因素。指令级别的优化实际上到处都有,如Spring框架、Mybatis框架等等,有时候只是我们不了解,所以看到了也不认识,也就不知道了。所以有时候刷剧的时间偶尔了解些”无用的东西“,对自己的技术之路或许会有意想不到的收获。
OK,咱们回聊~
jclasslib Bytecode Viewer插件使用说明:
1.安装插件 008650e0222a3151476f6d84e63b7b8.png 2.rebuild项目 e52d58a0033ad82522f84a15454649f.png 3.选中java文件,生成指令集 f7d91ebb41e150d220bef810d67f994.png 4.查看指令集 e0ed197789ecb8436ef5ae13a38d39a.png自己可以多尝试,多看,多理解慢慢就会了