J.U.C JMM. pipeline.指令重排序,happen-before

pipeline:html

      如今的CPU通常采用流水线方式来执行指令。一个指令执行周期被分红:取值,译码,执行,访存,写会,更新PC若干阶段。而后,多条指令能够同时存在于流水线中,同时被执行,来提升系统的吞吐量。缓存

      流水线并非串行的,并不会由于一个耗时很长的执行在"执行"阶段呆很长时间,而致使后续的指令被卡在"执行"阶段以前上。相反,流水线是并行的,多条指令能够同时处于同一阶段,只要CPU内部的处理部件未被占满既可。好比说CPU有一个加法器和一个除法器,那么一条加法指令和一条除法指令就可能同时处于"执行"阶段,而两条加法指令在"执行"阶段就只能被串行工做。优化

      然而,这样一来,乱序就可能产生了。好比一条加法指令原本出如今一条除法指令的后面,可是因为除法的执行时间很长,在他执行完以前,加法可能先执行完了,再好比两条访存指令,可能因为第二胎哦指令命中cache而致使他先于第一条指令完成。ui

     通常状况下,指令乱序并非CPU在执行指令以前刻意去调整顺序。CPU老是顺序的去内存里面取指令,而后将其顺序的方法指令流水线。可是指令执行时的各类条件,指令与指令之间的相互影响,可能致使顺序放入流水线的指令,最终乱序执行完成,这就是所谓的"顺序流入,乱序流出"。spa

    指令流水线除了在资源不足的状况下会卡主以外(如前所述的一个加法器应付两条加法指令的状况),指令之间的相关性也是致使流水线阻塞的重要缘由。.net

    CPU的乱序执行并非任意的乱序,而是以保障程序上下文因果关系为前提的。有了这个前提,CPU执行的正确性才有有保证:线程

a++; b = f(a); c--;

     因为b = f(a)这条指令依赖于前一条指令a++的执行结果,因此b = f(a)将在"执行"阶段以前被阻塞,知道a++的执行结果被生成出来;而c--跟前面没有依赖,他能够在b = f(a)以前就能执行完。像这样有依赖关系的指令若是挨着很近,后一条指令一定会由于等待前一条执行的结果,而在流水线中阻塞好久,占用流水线的资源。unix

     而编译器的乱序,做为编译优化的一种手段,则试图经过指令重排序将这样的两条指令拉开距离,以致于后一条指令进入CPU的时候,前一条指令结果已经能够获得了,那么也就不须要阻塞等待了,好比指令重拍为:code

a++ ; c-- ; b = f(a);

     相对于CPU的乱序,编译器的乱序才是真正的对指令顺序作了调整。可是编译器的乱序也必须保证程序上下文的因果关系不发生改变。htm

 

乱序的后果:

     乱序执行,有了"保证上下文英国关系"这一前提,通常状况下不会有什么问题的,所以,在绝大多数状况下,咱们写程序都不去考虑乱序所带来的影响。可是,有些程序逻辑,单纯从上下文是看不出他们的因果关系的。好比:

*addr = 5 ; val = *data;

     从表面上看,addr和data是没有什么联系的,彻底能够放心的去乱序执行,可是若是这是在xx设备驱动程序中,这两个变量可可能对应到设备的地址端口和数据端口。而且,这个设备规定了,当你须要读写设备上某个寄存器时,先将寄存器编号设置到地址端口,而后就能够经过对数据端口的读写而操做对应的寄存器,那么这么一来,对前面那两条指令的乱序执行就可能形成错误。对于这样的逻辑,咱们姑且将其称做隐式的因果关系;而指令与指令之间直接的输入输出依赖,称之为显式的因果关系。CPU或者编译器的乱序是以保证显式的因果关系不变为前提的,可是他们都没法识别隐式的因果关系。再举个例子:

object -> data = xxx;  object -> ready = true;

    当设置了data以后,记下标志,而后在另外一个线程中可能执行:

if (object -> ready) do_something(object -> data);

    若是考虑到乱序,若是标志被赋值先于data被赋值, 那么结果就可能杯具了,由于从字面上看,前面的那两条指令其实并不存在显式的因果关系,乱序是有可能发生的。

    总的来讲,若是程序有显式的因果关系的话,乱序必定会尊重这些关系;不然,乱序就可能打破程序原有的逻辑。这时候,就须要使用屏障来抑制乱序,以维持程序所指望的逻辑。

 

Memory barrier:

    内存屏障主要有:读屏障,写屏障,通用屏障,优化屏障;

    以读屏障为例,他用于保证读操做有序。屏障以前的读操做必定会先于屏障以后的读操做完成,写操做不受影响,同属于屏障的某一侧的读操做中也不受影响。相似的,写屏障用于限制写操做。而通用屏障则对读写操做都有做用。而优化屏障则用于限制编译器的指令重排,不区分读写。前三种屏障都隐含了优化屏障的功能,好比:

tmp = ttt ;  *addr = 5 ; memoryBarrier(); var = *data;

    有了内存屏障就能够确保先设置地址端口,再读取数据端口。而至于设置地址读卡和tmp的赋值孰先孰后,屏障则不作干预。

    有了内存屏障,就能够在隐式的因果关系的场景中,保证因果关系逻辑正确

 

多处理器状况:

      前面只是考虑了单处理器指令乱序的问题,而在多处理器下,除了每一个处理器要独自面对上面讨论的问题以外,当处理器以前存在交互的时候,一样要面对乱序的问题。

      一个处理器(记为a)对内存的写操做证并非直接就在内存上生效的,而是要先通过自身的cache。另外一个处理器(记为b)若是要独缺相应内存上的新值,先得等a的cache同步到内存,而后b的cache再从内存同步这个新值。而若是须要同步的值不止一个的话,就会存在顺序问题。再举前面的一个例子:

 <cpu - a>   *************************************** <cpu - b>
 object -> data = xxx;            
 write-memory-barrier();                          if (object -> ready)
 object -> ready = true;                          do_something(object -> data);

      前面也说过,必需要使用屏障来保证CPU-a不发生乱序,从而使得ready标记赋值时候,data必定是有效的。可是在多处理器状况下,这还不够。data和ready标记的新值可能以相反的顺序更新到CPU-b上!

      其实这种状况在大多数体系结构下并不会发生,不过内核文档memory-barriers举了一个alpha机器的例子。alpha机器可能使用分列的cache结构,每一个cache列能够并行工做,以提升效率。而每一个cache列上面的缓存的数据是互斥的(若是不互斥就还得解决cache列之间的一致性),因而就可能引起cache更新不一样步的问题。

      假设cahce被分为两列,而CPU-a和CPU-b上的data和ready都分别被缓存到不一样的cache列中。首先是CPU-a更新了cache以后,会发送消息让其余CPU的cache来同步新的值。可是如今假设了有两个cache列,可能因为缓存data的cache列比较繁忙而使得data的更新消息晚于ready发出,那么程序逻辑就无法保证了。不过在SMP下的内存屏障在解决指令乱序的问题在外,也将cache更新消息乱序的问题解决了。只要使用了屏障,就能保证屏障以前的cache更新消息先于屏障支护的消息被发出。

      而后就是CPU-b的问题。在使用了屏障以后,CPU-a已经保证data的更新消息先发出了,那么CPU-b也会先收到data的更新消息。不过一样,CPU-b上缓存data的cahce列可能比较繁忙,致使对data的更新晚于对ready的更新,这里一样会出问题。

      因此,在这种状况下,CPU-b也得使用屏障,CPU-a使用写屏障,保证两个写操做不乱序,而且相应的两个cache列的更新消息不乱序;CPU-b上则须要使用读屏障,保证对两个cache单元的同步不乱序,可见,SMP下的内存屏障必定是须要配对使用的。

 <cpu - a> ************************************************* <cpu - b>
 object -> data = xxx;                                      if (object -> ready)
 write-memory-barrier();                                    read-memory-barrier();
 object -> ready = true;                                     do_something(object -> data);

 

 原文:http://blog.csdn.net/jiang_bing/article/details/8629425

相关文章
相关标签/搜索