目录
定义
发生位置
数据依赖性
指令重排序的优缺点
处理器重排序规则
内存屏障类型
volatile防止指令重排序
定义
指令重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段,比如当2条指令顺序执行时,如果第一条指令操作的源数据不再寄存器中,则需要到内存中取,此时第2条指令就必须等第一条指令的数据取回并执行了才能执行。处理器对这种情况进行了优化,即如果第二条指令与第一条指令不存在数据依赖(相关性),且第二条指令的数据已经准备好,则第二条指令可以跳过第一条指令先执行。
即只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码逻辑顺序不一致,这个过程就叫做指令的重排序。
发生位置
指令重排序主要发生在编译器和处理器中。
- 编译器重排序:在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
- 处理器重排序:在程序不存在数据依赖性,且处理器允许的情况下,处理器能够对程序执行的机器指令进行重排序。
数据依赖性
如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。通俗点讲,就是当改变这两个操作的顺序时,程序的执行结果就会改变,此时这两个操作之间就是依赖的。数据依赖分为以下3种:
数据依赖类型表
名称 | 代码示例 | 说明 |
写后读 | a=1; b=a; | 写一个变量后,再读该变量 |
写后写 | a=1; a=2; | 写一个变量后,再写该变量 |
读后写 | a=b; b=1; | 读一个变量后,再写该变量 |
单线程下,若操作间存在数据依赖,则编译器和处理器在重排序时会遵守数据依赖性,不会改变存在数据依赖关系的两个操作的执行顺序,这是对as-if-serial语义的遵守;若操作间不存在数据依赖,则可能会被重排序。
指令重排序的优缺点
- 优点:使指令更加符合CPU的执行特性,最大限度的发挥机器的性能,提高程序的执行效率。
- 缺点:在多线程的情况下可能发生程序执行错误的情况。
多线程下,数据依赖性不被编译器和处理器考虑,即多线程下的指令重排序可能会改变程序的结果。举个例子:
class ReorderTest{
int a = 0;
boolean flag = false;
public void writer(){
a = 1; // 操作1
flag = true; // 操作2
}
public void reader(){
if (flag){ // 操作3
int i = a*a; // 操作4
....
}
}
writer方法中,a和flag两个变量没有数据依赖关系,则两个操作可以被指令重排序;reader方法中对a和flag进行了操作,与writer方法中的操作存在数据依赖关系,但编译器和处理器不考虑多线程下的数据依赖关系,所以writer和reader方法中的操作的顺序是未知的,不遵守as-if-serial语义。
假设线程1先执行writer方法,线程2然后执行reader方法,按顺序来看,操作1肯定是在操作2前面,即当flag=1时,a一定完成了1的赋值,i = 1。但在多线程和指令重排序的情况下可能会出现下面的情况:
线程1 | 线程2 |
flag=true; | |
| if(flag) |
| int i = a*a; |
a = 1; | |
此时i = 0,说明程序的结果被改变了。
处理器重排序规则
不同指令集的处理器对指令重排序的规则不一样,如下。其中最典型的X86架构的处理器仅仅支持Store-Load的执行重排序,因此在使用x86处理器的计算机上只需要使用内存屏障防止Store-Load类型的指令重排序即可。可以看到常见处理器都允许Store-Load重排序,都不允许对存在数据依赖的操作做重排序。
处理器(行) | 是否允许 | Load-Load | Load-Store | Store-Store | Store-Load | 数据依赖 |
SPARC-TSO | N | N | N | Y | N |
x86 | N | N | N | Y | N |
IA64 | Y | Y | Y | Y | N |
PowerPc | Y | Y | Y | Y | N |
内存屏障类型
JMM把内存屏障指令分为4类,每一种内存屏障能防止一种上下指令的重排序。下面的load指令代表对数据的读(普通变量或volatile变量),store代表对数据的写(普通变量或volatile变量)。
屏障类型 | 指令示例 | 说明 |
LoadLoad | Load1; LoadLoad; Load2 | 在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。 |
StoreStore | Store1; StoreStore; Store2 | 在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。 |
LoadStore | Load1; LoadStore; Store2 | 在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。 |
StoreLoad | Store1; StoreLoad; Load2 | 在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。StoreLoad屏障会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令。 |
volatile防止指令重排序
volatile能够保证对变量的写操作是立即可见的,或者说对volatile变量的读总是能读到最新的数据。除此之外,volatile关键字会限制指令重排序,具体规则如下: