指令重排序和內存屏障

  • 2019 年 10 月 15 日
  • 筆記

一、指令重排序

  指令重排序分為三種,分別為編譯器優化重排序指令級並行重排序內存系統重排序。如圖所示,後面兩種為處理器級別(即為硬件層面)。

  • 編譯器優化重排序:編譯器在不改變程序執行結果的情況下,為了提升效率,對指令進行亂序的編譯。例如在代碼中A操作需要獲取其他資源而進入等待的狀態,而A操作後面的代碼跟其沒有依賴關係,如果編譯器一直等待A操作完成再往下執行的話效率要慢的多,所以可以先編譯後面的代碼,這樣的亂序可以提升不小的編譯速度。
  • 指令級並行重排序:處理器在不影響程序執行結果的情況下,將多條指令重疊在一起執行,同樣也是為了提升效率。
  • 內存系統重排序:這個跟之前兩個不同的是,其為偽重排序,也就是說只是看起來像在亂序執行而已。對於現代的處理器來說,在CPU和主內存之間都具備一個高速緩存,高速緩存的作用主要為減少CPU和主內存的交互(CPU的處理速度要快的多),在CPU進行讀操作時,如果緩存沒有的話從主內存取,而對於寫操作都是先寫在緩存中,最後再一次性寫入主內存,原因是減少跟主內存交互時CPU的短暫卡頓,從而提升性能,但是延時寫入可能會導致一個問題——數據不一致。
    // CPU1執行以下操作  a = 1;  int i = b;    // CPU2執行下面操作  b = 1;  int j = a;

    其執行圖如下:

        

     

         從上面圖中我們可以看到,對於CPU來說,先將a = 1寫入緩存在讀取變量b,過後在寫入a到主內存,而這個操作從表面上看就變成了先讀取變量b,在寫入a到主內存,也就是發生了重排序,所以才說這為偽重排序。

            而從上面我們也可以看出,由於CPU1和2寫入的時機不同,最終可能導致讀到的(a,b)變量有四種情況,分別是(0,0),(0,1),(1,0),(1,1)。例如,在兩個緩存未寫入主內存的時候就進行變量讀取,這時候讀到的就為(0,0),其他情況類推。所以Java在實現內存模型的時候會禁止特定類型的重排序。

 

   as-if-serial語義:這是重排序都需要遵循的規則,其大致意思就是在單線程中,只要不改變程序的最終執行結果,那麼為了提升性能可以改變指令執行的順序。

二、內存屏障

  在編譯器方面使用volatile關鍵字可以禁止指令重排序,而在硬件方面實現禁止指令重排序的則是內存屏障。其中包括硬件層本來就有的LoadBarriers和StoreBarriers 和JVM封裝實現的四種內存屏障。

   從硬件層上

    內存屏障分為兩種,LoadBarriers和StoreBarriers。

    • LoadBarriers:在執行屏障後一個操作前,保證已經刷新了緩存的數據,也就是說使緩存失效,強制從內存刷新數據到緩存中。
      i = a;  LoadBarriers;  // ..其他操作

      如上偽代碼中,在執行其他操作之前必須保證a的變量從主內存中讀取並且刷新到緩存中。

    • StoreBarriers:此屏障之前的寫入緩存中的數據同步到內存中,並且保證其他線程可見。
      a = 1;  b = 2;  c = 3;  StoreBarriers;  // ..其他操作

      如上偽代碼中,保證在其他操作之前,寫入緩存中的a,b,c三個變量同步到主內存中,並且其他線程可以觀察到變量的變化。

  JVM實現的內存屏障

  1. LoadLoad:對於Load1;LoadLoad;Load2這樣的情況,保證Load1先於Load2及之後的Load操作,且對其可見。例如:
    ...  int i = a;  LoadLoad;  int j = b; 

    在這段代碼中,在int j = b以及後面的Load操作中,都能見到int i = a的操作,也就是int i = a先於後面的讀取操作。即,禁止int i = a和之後的讀操作重排序。

  2. LoadStore:對於Load1;LoadStore;Store1來說,保證Load1操作先於Store1以及後面的Store操作,即對後Store操作可見。如:
    int i = a;  LoadStore  b = 1;

    // int i = a對於b = 1及之後的store操作均可見。

  3. StoreLoad:同上,Store1;StoreLoad;Load1情況來說,保證Store1操作先於後續的所有Load操作,並且其Store的變量操作對其他處理器可見。由於Store操作會立即刷新到內存並對其他處理器緩存可見的特性,其具備其他三個屏障的功能,但是相對的,其花費的開銷較大。
  4. StoreStore:在Store1;StoreStore;Store2情況中,保證Store1操作先於Store2操作,即在Store1後續的Store操作之前,Store1操作保證刷新到內存並且對其他處理器可見。

  volatile的禁止指令重排序

    我們都知道volatile關鍵字有兩個語義:

    • 保證內存可見性
    • 禁止指令重排序

    其中JVM對其禁止指令重排序在硬件層面的實現就是通過在volatile修飾的變量前後插入內存屏障。volatile變量的內存屏障規則如下:

  在每個volatile寫操作前插入StoreStore屏障,在寫操作後插入StoreLoad屏障;
  在每個volatile讀操作前插入LoadLoad屏障,在讀操作後插入LoadStore屏障;

    而在編譯器方面則是因為對於volatile變量內存中的六種操作會有特殊的規則,可以看看我的另一篇文章——淺談內存模型,裏面介紹了volatile兩種語義的原理,同時也說明了volatile關鍵字沒有原子性的原因。

 

 

 文章若有不正之處,還望指出,在此多謝!