重排序与数据依赖性

上一篇博客我们了解了Java内存模型,下面我们来了解一下重排序和数据依赖性的相关知识。

文章目录

为什么需要重排序

现在的CPU一般采用流水线来执行指令。一个指令的执行被分成:取指、译码、访存、执行、写回、等若干个阶段。然后,多条指令可以同时存在于流水线中,同时被执行。

指令流水线并不是串行的,并不会因为一个耗时很长的指令在“执行”阶段呆很长时间,而导致后续的指令都卡在“执行”之前的阶段上。我们编写的程序都要经过优化后(编译器和处理器会对我们的程序进行优化以提高运行效率)才会被运行,优化分为很多种,其中有一种优化叫做重排序,重排序需要遵守as-if-serial规则和happens-before规则,不能说你想怎么排就怎么排,如果那样岂不是乱了套。重排序的目的是为了性能

Example:

过程A:cpu0—写入1> bank0;
过程B:cpu0—写入2> bank1;
如果bank0状态为busy, 则A过程需要等待
如果进行重排序,则直接可以先执行B过程。

重排序分类

一般重排序可以分为如下三种:

  1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;
  2. 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序;
  3. 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的。

重排序过程

一个好的内存模型实际上会放宽对处理器和编译器规则的束缚,也就是说软件技术和硬件技术都为同一个目标而进行奋斗:在不改变程序执行结果的前提下,尽可能提高并行度。Java内存模型(JMM)对底层尽量减少约束,使其能够发挥自身优势。因此,在执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排序

《重排序与数据依赖性》

如图,1属于编译器重排序,而2和3统称为处理器重排序。这些重排序会导致线程安全的问题,一个很经典的例子就是DCL(双重检验锁)问题,这个在以后的文章中会具体去聊。针对编译器重排序,Java内存模型(JMM)的编译器重排序规则会禁止一些特定类型的编译器重排序;针对处理器重排序,编译器在生成指令序列的时候会通过插入内存屏障指令来禁止某些特殊的处理器重排序

那么什么情况下,不能进行重排序了?下面就来说说数据依赖性。有如下代码:

double pi = 3.14 //A
double r = 1.0   //B
double area = pi * r * r //C

这是一个计算圆面积的代码,由于A,B之间没有任何关系,对最终结果也不会存在影响,它们之间执行顺序可以重排序。因此执行顺序可以是A->B->C或者B->A->C执行最终结果都是3.14,即A和B之间没有数据依赖性。具体的定义为:如果两个操作访问同一个变量,且这两个操作有一个为写操作,此时这两个操作就存在数据依赖性,这里就存在三种情况:1. 读后写;2.写后写;3. 写后读,或者三种操作都是存在数据依赖性的,如果重排序会对最终执行结果产生影响,编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖性关系的两个操作的执行顺序

重排序对多线程的影响

class ReorderExample { 
    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
            ……
        }
    }
}

flag为标志位,表示a有没有被写入,当A线程执行 writer 方法,B线程执行 reader 方法,线程B在执行4操作的时候,能否看到线程A对a的写入操作?

答案是:不一定!

由于操作1和操作2没有数据依赖关系,编译器和处理器可以对这两个操作重排序。

如果操作1和操作2做了重排序,程序执行时,线程A首先写标记变量 flag,随后线程 B 读这个变量。由于条件判断为真,线程 B 将读取变量a。此时,变量 a 还根本没有被线程 A 写入,在这里多线程程序的语义被重排序破坏了

数据依赖性

如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。数据依赖分下列三种类型:

名称代码示例说明
写后读a = 1;b = a;写一个变量之后,再读这个位置。
写后写a = 1;a = 2;写一个变量之后,再写这个变量。
读后写a = b;b = 1;读一个变量之后,再写这个变量。

上面三种情况,只要重排序两个操作的执行顺序,程序的执行结果将会被改变。前面提到过,编译器和处理器可能会对操作做重排序。编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。注意,这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑。如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。所以有数据依赖性的语句不能进行重排序。

    原文作者:ThinkWon
    原文地址: https://blog.csdn.net/ThinkWon/article/details/102073858
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞