摘要:那么我们就应该问问为啥要用指令重排序呢生活类比我们从生活中举个例子,假设你有一箱红纸,现在要你剪成小红花贴在窗上。
我们知道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://www.ucloud.cn/yun/69559.html
摘要:安全性小结我们上边介绍了原子性操作内存可见性以及指令重排序三个在多线程执行过程中会影响到安全性的问题。 指令重排序 如果说内存可见性问题已经让你抓狂了,那么下边的这个指令重排序的事儿估计就要骂娘了~这事儿还得从一段代码说起: public class Reordering { private static boolean flag; private static in...
摘要:处理器通过缓存能够从数量级上降低内存延迟的成本这些缓存为了性能重新排列待定内存操作的顺序。从上述触发步骤中,可以看到第步发生了指令重排序,并导致第步读到错误的数据。内存屏障是用来防止出现指令重排序的利器之一。 这两天,我拜读了 Dennis Byrne 写的一片博文Memory Barriers and JVM Concurrency (中译文内存屏障与JVM并发)。 文中提到: ...
摘要:前提深入理解内存模型程晓明著,该书在以前看过一遍,现在学的东西越多,感觉那块越重要,于是又再细看一遍,于是便有了下面的读书笔记总结。同步同步是指程序用于控制不同线程之间操作发生相对顺序的机制。线程之间的通信由内存模型控制。 showImg(https://segmentfault.com/img/remote/1460000013474312?w=1920&h=1271); 前提 《深...
摘要:线程之间的通信由内存模型本文简称为控制,决定一个线程对共享变量的写入何时对另一个线程可见。为了保证内存可见性,编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。 并发编程模型的分类 在并发编程中,我们需要处理两个关键问题:线程之间如何通信及线程之间如何同步(这里的线程是指并发执行的活动实体)。通信是指线程之间以何种机制来交换信息。在命令式编程中,线程之间的...
摘要:前提深入理解内存模型程晓明著,该书在以前看过一遍,现在学的东西越多,感觉那块越重要,于是又再细看一遍,于是便有了下面的读书笔记总结。同步同步是指程序用于控制不同线程之间操作发生相对顺序的机制。线程之间的通信由内存模型控制。 showImg(https://mmbiz.qpic.cn/mmbiz_jpg/1flHOHZw6RtPu3BNx3zps1JhSmPICRw7QgeOmxOfTb...
摘要:注意,禁止指令重排序在之后才被修复使用局部变量优化性能重新查看中双重检查锁定代码。帮助文档双重检查锁定与延迟初始化有关双重检查锁定失效的说明 双重检查锁定(Double check locked)模式经常会出现在一些框架源码中,目的是为了延迟初始化变量。这个模式还可以用来创建单例。下面来看一个 Spring 中双重检查锁定的例子。 showImg(https://segmentfaul...
阅读 3137·2021-09-30 09:47
阅读 1965·2021-09-22 16:04
阅读 2240·2021-09-22 15:44
阅读 2518·2021-08-25 09:38
阅读 524·2019-08-26 13:23
阅读 1203·2019-08-26 12:20
阅读 2793·2019-08-26 11:59
阅读 1064·2019-08-23 18:40