天天看點

了解 x86 架構下的記憶體屏障和重排序問題

作者:ZeaTalk
了解 x86 架構下的記憶體屏障和重排序問題

在之前的文章中我們提到一個問題, final 字段的寫入與構造方法傳回之前,編譯器會插入一個 StoreStore 屏障;同樣,在 volatile 字段的寫入之前,也會插入一個 StoreStore 屏障,但你是否有想過,這個屏障在 x86 的架構下為什麼是no-op(空操作)呢?

這不得不從 x86 的 TSO(Total Store Order)[1] 模型說起。

論文[1]中,作者在非數學層面用四句話來概括什麼是 x86-TSO:

  • 首先 store buffers 被設計成了 FIFO 的隊列,如果某個線程需要讀取記憶體裡的變量,務必優先讀取本地 store buffer 中的值(如果有的話),否則去主記憶體裡讀取;
  • MFENCE 指令用于清空本地 store buffer,并将資料刷到主記憶體;
  • 某線程執行 Lock 字首的指令集時,會去争搶全局鎖,拿到鎖後其他線程的讀取操作會被阻塞,在釋放鎖之前,會清空該線程的本地的 store buffer,這裡和 MFENCE 執行邏輯類似;
  • store buffers 被寫入變量後,除了被其他線程持有鎖以外的情況,在任何時刻均有可能寫回記憶體。
了解 x86 架構下的記憶體屏障和重排序問題

上面給出的圖檔是否似曾相識?在我的另一篇文章裡介紹過 store buffer 與其他 CPU 元件的基本結構,但是略有不同的是,這張圖忽略了 CPU 緩存的存在,這是因為作者為了更好闡述 x86-TSO 模型而給我們呈現的抽象圖示,不涉及到具體的 CPU 構件,這裡的 thread 其實和 CPU 的 Processor 是一一對應的,這裡的 write buffer,實質上就是 store buffer。

根據上面的 x86-TSO 模型,我們可以推測出 x86 架構下是不需要 StoreStore 屏障的,試想一下,x86 的 store buffer 被設計成了 FIFO,縱然在同一個線程中執行多次寫入 buffer 的操作,最終依舊是嚴格按照 FIFO 順序 dequeue 并寫回到記憶體裡,自然而然,對于其他任何線程而言,所『看到』的該線程的變量寫回順序是和指令序列一緻的,是以不會出現重排序。

進一步思考,我們知道讀屏障就是為了解決 invalidate queue 的引入導緻資料不一緻的問題,x86-TSO 模型下是沒有 invalidate queue 的,是以也不需要讀屏障(LoadLoad[2])。

那麼 LoadStore 呢?我們這裡用個例子直覺分析下:

a,b=0;
c=2;

// proc 0
void foo(){
  assert b == 0;
  c=b;
  a=2;
  b=1;
}

// proc 1
void bar(){
 if(a==2){
   assert c == 0;
 }
}           

實際上這個斷言的通過需要依賴兩個屏障,LoadStore 和 StoreStore,我們上面讨論了 x86 不需要 StoreStore Barrier,是以這相當于是 no-op;我們也就隻需要分析 LoadStore Barrier 也是 no-op 的情況下,上面兩個斷言是否一定能通過?

我的分析是這樣的,foo 方法中 c、a、b 三個變量順序寫入,是不會被重排序的,這是由 store buffer FIFO 特性所決定。換句話說,b=1 不會被重排序到 c=b 之前,是以 bar 方法的斷言通過;

那 b=1 是否會重排序到 assert b == 0 之前呢?我們知道 x86-TSO 要求變量讀取優先檢查 store buffer ,如果不存在則去主記憶體尋址,根據這套順序,在執行 assert b==0 的時候 b 唯一可能就是 0,是以 foo 方法斷言通過。

根據上面的分析,我們可以推論 LoadStore Barrier 也是 no-op。

最後,StoreLoad,這或許是 x86-TSO 模型下唯一需要考慮的重排序場景(排除編譯器優化重排序)。雖然store buffer 是 FIFO,但整體架構本質依然是最終一緻性而非線性一緻性。這勢必會出現在某個時間節點,不同處理器看到的變量不一緻的情況。繼續看下面的僞代碼:

x,y=0;
// proc 0
void foo(){
  x=1;
  read y;
}

// proc 1
void bar(){
  y=1;
  read x;
}           

如果遵循線性一緻性,我們大可以枚舉可能發生的情況,但無論怎麼枚舉,都不可能是 x=y=0,然而詭異之處在于 x86-TSO 模型下是允許 x=y=0這種情況存在的。結合上面的抽象圖示分析,x86 在 StoreLoad 的場景下允許 x=y=0 發生,也就不難了解了。以 foo 方法為例,由于 y=1 的寫入有可能還停留在 proc1 的 store buffer 中,foo 方法末尾讀到的 y 可能是舊值,同理 bar 方法末尾也有可能讀到 x 的舊值,那麼讀出來自然有可能是 x=y=0。

如何禁止 StoreLoad 重排序?

在 x86-TSO 裡提到 MFENCE,這個指令用于強制清空本地 store buffer,并将資料刷到主記憶體,本質上就是 StoreLoad Barrier。

This serializing operation guarantees that every load and store instruction that precedes the MFENCE instruction in program order becomes globally visible before any load or store instruction that follows the MFENCE instruction. MFENCE - Memory Fence

MFENCE 保證了在 MFENCE 指令執行前的讀寫操作對全局可見,可見這是一個很重的屏障,這也是 StoreLoad Barrier 所需要的。除此之外 x86-TSO 還提到 LOCK 字首的相關指令,在釋放鎖的過程中有和 MFENCE 類似的過程,同樣能達到 StoreLoad Barrier 的效果。

而 Hotspot VM 選擇了 LOCK 指令作為 StoreLoad 屏障,這又是為何呢?Hotspot 源碼中給出了這個注釋:

enum Membar_mask_bits {
    StoreStore = 1 << 3,
    LoadStore  = 1 << 2,
    StoreLoad  = 1 << 1,
    LoadLoad   = 1 << 0
  };

  // Serializes memory and blows flags
  void membar(Membar_mask_bits order_constraint) {
    if (os::is_MP()) {
      // We only have to handle StoreLoad
      if (order_constraint & StoreLoad) {
        // All usable chips support "locked" instructions which suffice
        // as barriers, and are much faster than the alternative of
        // using cpuid instruction. We use here a locked add [esp-C],0.
        // This is conveniently otherwise a no-op except for blowing
        // flags, and introducing a false dependency on target memory
        // location. We can't do anything with flags, but we can avoid
        // memory dependencies in the current method by locked-adding
        // somewhere else on the stack. Doing [esp+C] will collide with
        // something on stack in current method, hence we go for [esp-C].
        // It is convenient since it is almost always in data cache, for
        // any small C.  We need to step back from SP to avoid data
        // dependencies with other things on below SP (callee-saves, for
        // example). Without a clear way to figure out the minimal safe
        // distance from SP, it makes sense to step back the complete
        // cache line, as this will also avoid possible second-order effects
        // with locked ops against the cache line. Our choice of offset
        // is bounded by x86 operand encoding, which should stay within
        // [-128; +127] to have the 8-byte displacement encoding.
        //
        // Any change to this code may need to revisit other places in
        // the code where this idiom is used, in particular the
        // orderAccess code.

        int offset = -VM_Version::L1_line_size();
        if (offset < -128) {
          offset = -128;
        }

        lock();
        addl(Address(rsp, offset), 0);// Assert the lock# signal here
      }
    }
  }           

Hotspot 采用的是 locked add [esp-C],0 ,其最主要的出發點還是性能。

小結

由于 x86 是遵循 TSO 的最終一緻性模型,如若出現 data race 的情況還是需要考慮同步的問題,尤其是在 StoreLoad 的場景。而其餘場景由于其 store buffer 的特殊性以及不存在 invalidate queue 的因素,可以不需要考慮重排序的問題,是以在 x86 平台下,除了 StoreLoad Barrier 以外,其餘的 Barrier 均為空操作。

參考資料:

[1]《x86-TSO: A Rigorous and Usable Programmer's Model for x86 Multiprocessors》

[2]https://stackoverflow.com/questions/15360598/what-does-a-loadload-barrier-really-do

[3]https://paulcavallaro.com/blog/x86-tso-a-programmers-model-for-x86-multiprocessors/

[4]https://mp.weixin.qq.com/s?__biz=MzUzMDk3NjM3Mg==&mid=2247483755&idx=1&sn=50f80e73f46fab04d8a799e8731432c6&chksm=fa48da70cd3f5366d9658277cccd9e36fca540276f580822d41aef7d8af4dda480fc85e3bde4&token=630636109&lang=zh_CN#rd

[5]https://www.cl.cam.ac.uk/~pes20/weakmemory/index3.html

[6]https://mp.weixin.qq.com/s?__biz=MzUzMDk3NjM3Mg==&mid=2247483755&idx=1&sn=50f80e73f46fab04d8a799e8731432c6&chksm=fa48da70cd3f5366d9658277cccd9e36fca540276f580822d41aef7d8af4dda480fc85e3bde4&token=1422563498&lang=zh_CN#rd

[7]https://software.intel.com/en-us/forums/intel-moderncode-for-parallel-architectures/topic/304284

[8]http://www.4e00.com/blog/java/2018/10/21/inside-java-memory-model.html

[9]https://github.com/leonlibraries/openjdk-10/blob/master/hotspot/src/cpu/x86/vm/assembler_x86.hpp#L1307

繼續閱讀