计算机在执行程序时,为了提高性能,编译器和处理器尝尝会对指令做重排

编译器优化重排
编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。属于编译器重排
// 优化前
int x = 1;
int y = 2;
int a1 = x * 1;
int b1 = y * 1;
int a2 = x * 2;
int b2 = y * 2;
// 优化后
int x = 1;
int y = 2;
int a1 = x * 1;
int a2 = x * 2;
int b1 = y * 1;
int b2 = y * 2;
CPU只读一次的x和y值,不需反复读取寄存器来交替x和y值
指令并行的重排
现代处理器采用了指令级并行技术来讲多条指令重叠执行。如果不存在数据依赖性(即后一个执行的语句无需依赖前面执行语句的结果),处理器可以改变语句对应的机器指令的执行顺序。属于处理器重排
从指令的执行角度来说,一条指令可以分为多个步骤完成:
取指:IF
译码和取寄存器操作数:ID
执行或者有效地址计算:EX(ALU逻辑计算单元)
存储器访问:MEM
写回寄存器:WB
CPU在工作时,需要将上述指令分为多个步骤依次执行,由于每一步会使用到不同的硬件操作,比如取指会只有PC寄存器和存储器,译码会执行到指令寄存器组,执行时会执行ALU、写回时使用到寄存器组。为了提高硬件使用率,CPU指令是按流水线来执行

虽然流水线可以大大提升CPU的性能,但是一旦出现流水中断,所有硬件将会进入一轮停顿期,当再次弥补中断点可能需要几个周期,这种性能损失也会很大,因此需要尽量阻止指令中断的情况,指令重排就是其中一种优化中断的手段。
例如:
i = a + b;
y = c - d;

上述执行过程在某些指令上存在X的标志,X代表中断的含义。为什么停顿呢?这是因为部分数据还没有准备好,如执行ADD指令时,需要使用到前面指令的数据R1、R2,而此时R2的MEM操作还没有完成。想要消除这些停顿,就要用到指令重排。如下图,既然ADD指令需要等待,那就利用等待的时间做些别的事,如把
LW R4,c
和LW R5,d
移动到前面执行,毕竟LW R4,c
和LW R5,d
执行并没有数据依赖关系
内存系统的重排
由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行,因为缓存的存在,导致内存和缓存的数据同步存在时间差。属于处理器重排
CPU缓存一致性协议,例如MESI,多个CPU核心之间缓存不会出现不同步的问题,不会有“内存可见性”问题。但缓存一致性协议对性能有很大损耗,为了解决这个问题,又进行了各种优化,例如增加Store Buffer、Load Buffer等。Store Buffer的延迟写入是重排序的一种,也称之为“内存重排序”,是造成“内存可见性”问题的主因。
// 线程1 线程2
X = 1; Y = 1;
a = Y; b = X;
假设X、Y是两个全局变量,初始时候,X = 0, Y = 0。线程1和线程2的执行先后顺序是不确定,可能顺序执行,也可能交叉执行,最终结果可能是:
1. a = 0, b = 1
2. a = 1, b = 0
3. a = 1, b = 1
但实际上还有一种可能是:a = 0, b = 0
两个线程的指令都没有重排序,执行顺序就是代码的顺序,但仍然可能出现a=0, b=0。原因是线程1先执行X=1后执行a=Y,但此时X=1还在自己的Store Buffer里面,没有及时写入主内存中。所以线程2看到的X还是0。线程2的道理与此相同。
虽然线程1觉得自己是按代码顺序正常执行的,但在线程2看来,a=Y和X=1顺序却是颠倒的。指令没有重排序,是写入内存的操作被延迟了,也就是内存被重排序了,这就造成内存可见性问题。
解决方案
为了保证内存可见性,Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序
网友评论