周末,小伙伴召威进行了面试题重排序的分享,听完收获颇丰
并下来自己跑了下代码进行了些学习
// 面试题:定义了A=0,B=0;a=0,b=0四个全局变量。设计两个线程,线程1执行a=1;A=b; 线程2执行b=1;B=a; // // 要求一:两个线程遵循Happens-Before原则 // 要求二:保证两个线程都执行完后打印出A的值,B的值(注:列举出A,B的所有可能的取值组合,并解释每一种组合发生的原因)
以下几种可能的方案:
// 第一种 A=0,B=1 thread1在thread2之前先执行(a=1,A=0; b=1,B=1) // 第二种 A=1,B=0 thread2在thread1之前先执行(b=1,B=0; a=1,A=1) // 第三种 A=1,B=1 thread1在thread2同时执行 (a=1,b=1; A=1,B=1) // 第四种 A=0,B=0 指令充排序,几率很小,我跑了5分钟也没跑出来,但确实有这个可能性
代码示例,来自召威,我进行了一些补充说明
package pers.wmx.springbootfreemarkerdemo;
import java.util.concurrent.CountDownLatch;
/**
* @author: wangmingxin03
* @date: 2020-04-13
*/
public class ReorderingCode {
private static int A=0,B=0;
//volatile 防止指令重排序
private volatile static int a=0,b=0;
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch=new CountDownLatch(1);
int sum=0;
for (;;){
// A B a b重新置为0,避免之前的赋值影响后续的结果
A=0;
B=0;
a=0;
b=0;
sum++;
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
a=1;
A=b;
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
b=1;
B=a;
}
});
thread1.start();
thread2.start();
//放行 目的是为了让thread1、thread2都启动
countDownLatch.countDown();
//目的是为了让thread1、thread2 都执行完,在执行主程序
thread1.join();
thread2.join();
// if (A==0 && B==0){
// System.out.println("执行了"+sum+"次"+"A="+A+","+"B="+B);
// break;
// };
if (A==1 && B==1){
System.out.println("执行了"+sum+"次"+"A="+A+","+"B="+B);
break;
};
}
}
}
大多数现代微处理器都会采用将指令乱序执行(out-of-order execution,简称OoOE或OOE)的方法,在条件允许的情况下,直接运行当前有能力立即执行的后续指令,避开获取下一条指令所需数据时造成的等待。通过乱序执行的技术,处理器可以大大提高执行效率。
除了处理器,常见的Java运行时环境的JIT编译器也会做指令重排序操作,即生成的机器指令与字节码指令顺序不一致。
As-if-serial语义的意思是,所有的动作(Action)都可以为了优化而被重排序,但是必须保证它们重排序后的结果和程序代码本身的应有结果是一致的。Java编译器、运行时和处理器都会保证单线程下的as-if-serial语义。 比如,为了保证这一语义,重排序不会发生在有数据依赖的操作之中。
- int a = 1;
- int b = 2;
- int c = a + b;
将上面的代码编译成Java字节码或生成机器指令,可视为展开成了以下几步动作(实际可能会省略或添加某些步骤)。
- 对a赋值1
- 对b赋值2
- 取a的值
- 取b的值
- 将取到两个值相加后存入c
在上面5个动作中,动作1可能会和动作2、4重排序,动作2可能会和动作1、3重排序,动作3可能会和动作2、4重排序,动作4可能会和1、3重排序。但动作1和动作3、5不能重排序。动作2和动作4、5不能重排序。因为它们之间存在数据依赖关系,一旦重排,as-if-serial语义便无法保证。
编译器进行重排序的目的是减少从内存中读取的时间,通过进行指令重排序,要用到的值已经在寄存器中就省去到内存中读取的时间;
一个简单的例子,下面代码块A的性能不如代码块B好。
//代码块A int a = 1; int b = 1; a = a + 1; b = b + 1; //代码块B int a = 1; a = a + 1; int b = 1; b = b + 1;
处理器为啥要重排序?因为一个汇编指令也会涉及到很多步骤,每个步骤可能会用到不同的寄存器,CPU使用了流水线技术,也就是说,CPU有多个功能单元(如获取、解码、运算和结果),一条指令也分为多个单元,那么第一条指令执行还没完毕,就可以执行第二条指令,前提是这两条指令功能单元相同或类似,所以一般可以通过指令重排使得具有相似功能单元的指令接连执行来减少流水线中断的情况。
重排序出现在计算机世界的各个方面:编译器对指令重排序、JVM在执行时进行重排序(JIT会对频繁调用的热代码翻译成本地机器码后执行)、CPU的重排序(减少内存的访问、增加缓存的使用)。
JAVA指令重排序是指编译器对于指令的重排序,这个重排序会因为编译器不同而不同,然而它们的根本目的只有一个:提高代码执行的效率,主要是时间效率。
指令重排序主要有两个小的目标:
(1)增加内存访问效率;
(2)减少代码跳转。
在第一个目标中,JAVA通过重排序,将从相邻内存地址的读指令放在一起,进而优化了内存访问效率;
在第二个目标中,通过优化可以减少代码的跳转,这类优化经常和逻辑相关。
指令重排序带来收益的理由根源是JMM是一种松散一致性模型,松散一致模型的具体表现通过happen-before的规则来实现一致性。
这种模型中,编译器只要遵守这些规则,那么编译器就有极大的自由进行指令重排序和删掉不必要的同步代码。
这种自由使得编译器可以通过产生更好的指令顺序以优化内存访问、优化逻辑。
转载请注明:汪明鑫的个人博客 » 编译器优化手段 重排序
说点什么
您将是第一位评论人!