我们知道 java 在运行的时候有两个地方可能用到重排序, 一个是编译器编译的的时候, 一个是处理器运行的时候. 那么我们就应该问问为啥要用指令重排序呢?
生活类比
我们从生活中举个例子, 假设你有一箱红纸, 现在要你剪成小红花贴在窗上. 你有两种极端的选择: 拿出来一个, 把这个剪好, 再贴上去...... 一个一个依次进行; 另一种方式是先全部拿出来, 然后全部剪好, 最后全部贴上去.
那种效率更高? 很明显是后者, 因为前者你就需要不停地在箱子, 剪刀和胶水之间切换, 这个切换过程不仅浪费时间, 还耗费精力. 但是后者一直做一个工作也很无聊, 还会导致半天了窗上一朵花都没有, 会给你带来失落感, 所以比较合适的做法就是拿出来一叠, 把这一叠剪好, 贴上去. 这样既不无聊, 也减少了切换次数, 提高了工作效率.
再想想, 如果有三个人, 一个负责拿, 一个负责剪, 一个负责贴, 就更快了.
分析
编译期重排序有啥好处? CPU 计算的时候要访问值, 如果常常利用到寄存器中已有的值就不用去内存读取了, 比如说
- int a = 1;
- int b = 1;
- a = a + 1;
- b = b +1 ;
就可能没有
- int a = 1;
- a = a + 1;
- int b = 1;
- b = b +1 ;
性能好, 因为后者可以 a 或 b 可能在寄存器中了.
处理器为啥要重排序? 因为一个汇编指令也会涉及到很多步骤, 每个步骤可能会用到不同的寄存器, CPU 使用了流水线技术, 也就是说, CPU 有多个功能单元 (如获取, 解码, 运算和结果), 一条指令也分为多个单元, 那么第一条指令执行还没完毕, 就可以执行第二条指令, 前提是这两条指令功能单元相同或类似, 所以一般可以通过指令重排使得具有相似功能单元的指令接连执行来减少流水线中断的情况.
我们写一段代码来试试:
- package *****;
- /**
- * reorder
- * @author Mageek Chiu
- * @date 2018/5/25 0025:12:49
- */
- public class ReOrder {
- public int value ;
- private ReOrder(int value) {
- this.value = value;
- }
- public static void main(String... args){
- ReOrder reOrder = new ReOrder(111);
- ReOrder reOrder1 = new ReOrder(222);
- ReOrder reOrder2 = new ReOrder(333);
- System.out.println(add1(reOrder,reOrder1,reOrder2));
- }
- static int add1(ReOrder reOrder,ReOrder reOrder1,ReOrder reOrder2){
- int result = 0;
- result += reOrder.value;
- result += reOrder1.value;
- result += reOrder2.value;//***
- result += reOrder.value;
- result += reOrder1.value;
- result += reOrder2.value;
- result += reOrder.value;
- result += reOrder1.value;
- result += reOrder2.value;
- return result;
- }
- }
运行结果中:
- # {method} {0x000000001c402c80} 'add1' '(*****/ReOrder;*****/ReOrder;*****/ReOrder;)I' in '*****/ReOrder'
- # parm0: rdx:rdx = '*****/ReOrder'
- # parm1: r8:r8 = '*****/ReOrder'
- # parm2: r9:r9 = '*****/ReOrder'
- # [sp+0x20] (sp of caller)
0x00000000032a86c0: mov dword ptr [rsp+0ffffffffffffa000h],eax
0x00000000032a86c7: push rbp
0x00000000032a86c8: sub rsp,10h ;*synchronization entry
; - *****.ReOrder::add1@-1 (line 24)
0x00000000032a86cc: mov r11d,dword ptr [rdx+0ch]
- ;*getfield value
- ; - *****.ReOrder::add1@4 (line 26)
; implicit exception: dispatches to 0x00000000032a86ff
0x00000000032a86d0: mov r10d,dword ptr [r8+0ch] ;*getfield value
; - *****.ReOrder::add1@11 (line 27)
; implicit exception: dispatches to 0x00000000032a870d
0x00000000032a86d4: mov r9d,dword ptr [r9+0ch] ;*getfield value
; - *****.ReOrder::add1@18 (line 28)
; implicit exception: dispatches to 0x00000000032a8719
0x00000000032a86d8: mov eax,r11d
0x00000000032a86db: add eax,r10d
0x00000000032a86de: add eax,r9d
0x00000000032a86e1: add eax,r11d
0x00000000032a86e4: add eax,r10d
0x00000000032a86e7: add eax,r9d
0x00000000032a86ea: add eax,r11d
0x00000000032a86ed: add eax,r10d
0x00000000032a86f0: add eax,r9d ;*iadd
也就是先用 mov 把方法里面所需要的三个 value 加载了, 再统一用 add 进行加法运算.
现在我们把 //*** 哪一行注释掉, 运行结果如下:
- [Constants]
- # {method} {0x000000001c052c78} 'add1' '(*****/ReOrder;*****/ReOrder;*****/ReOrder;)I' in '*****/ReOrder'
- # parm0: rdx:rdx = '*****/ReOrder'
- # parm1: r8:r8 = '*****/ReOrder'
- # parm2: r9:r9 = '*****/ReOrder'
- # [sp+0x20] (sp of caller)
0x0000000002f47d40: mov dword ptr [rsp+0ffffffffffffa000h],eax
0x0000000002f47d47: push rbp
0x0000000002f47d48: sub rsp,10h ;*synchronization entry
; - *****.ReOrder::add1@-1 (line 24)
0x0000000002f47d4c: mov r11d,dword ptr [rdx+0ch]
- ;*getfield value
- ; - *****r.ReOrder::add1@4 (line 26)
; implicit exception: dispatches to 0x0000000002f47d7c
0x0000000002f47d50: mov r10d,dword ptr [r8+0ch] ;*getfield value
; - *****.ReOrder::add1@11 (line 27)
; implicit exception: dispatches to 0x0000000002f47d89
0x0000000002f47d54: mov r9d,dword ptr [r9+0ch] ;*getfield value
; - *****::add1@32 (line 32)
; implicit exception: dispatches to 0x0000000002f47d95
0x0000000002f47d58: mov eax,r11d
0x0000000002f47d5b: add eax,r10d
0x0000000002f47d5e: add eax,r11d
0x0000000002f47d61: add eax,r10d
0x0000000002f47d64: add eax,r9d
0x0000000002f47d67: add eax,r11d
0x0000000002f47d6a: add eax,r10d
0x0000000002f47d6d: add eax,r9d ;*iadd
依然是先把所有 value 都用 mov 指令加载后再进行加法运算. 总结起来就是不管代码里这个值使用顺序多靠后, 都先用 mov 加载后再使用 add 对这个值进行运算.
注意, 上面的运行参数为
-Xcomp -XX:+UnlockDiagnosticVMOptions -XX:CompileCommand=print,*ReOrder.add1 -XX:+PrintCompilation
. Xcomp 含义是使用编译模式而不是解释模式,
-XX:CompileCommand=print,*ReOrder.add1
表示只打印这个方法,
-XX:+PrintCompilation
表示打印方法名称. 需要插件 hsdis, 编译好后放在
jdk 的 jre 的 bin 的 server
中就好, 具体环境搭建可以参阅这里 https://stackoverflow.com/questions/1503479/how-to-see-jit-compiled-code-in-jvm
分析不对的地方请轻拍. 访问原文 http://mageek.cn/archives/99/
来源: https://juejin.im/post/5b0b56f6f265da0dd6488083