主記憶體屏障

主記憶體屏障(英語:Memory barrier),也稱主記憶體柵欄主記憶體柵障屏障指令等,是一類同步屏障指令,它使得 CPU 或編譯器在對主記憶體進行操作的時候, 嚴格按照一定的順序來執行, 也就是說在主記憶體屏障之前的指令和之後的指令不會由於系統最佳化等原因而導致亂序。

大多數現代電腦為了提高效能而採取亂序執行,這使得主記憶體屏障成為必須。

語意上,主記憶體屏障之前的所有寫操作都要寫入主記憶體;主記憶體屏障之後的讀操作都可以獲得同步屏障之前的寫操作的結果。因此,對於敏感的程式塊,寫操作之後、讀操作之前可以插入主記憶體屏障。

舉例

當驅動程式執行下列動作時,如果處理器的寫入指令 out-of-order,使得資料還沒有寫入記憶體,硬體模組就被觸發開始動作,就會產生錯誤的行為。

  寫資料到記憶體, 稍後硬體模塊會存取這一筆資料
  // 此處需要內存屏障
  觸發硬體模塊開始處理資料

底層體系結構相關的原語

大多數處理器提供了主記憶體屏障指令:

  • 完全主記憶體屏障(full memory barrier)保障了早於屏障的主記憶體讀寫操作的結果提交到主記憶體之後,再執行晚於屏障的讀寫操作。
  • 主記憶體讀屏障(read memory barrier)僅確保了主記憶體讀操作;
  • 主記憶體寫屏障(write memory barrier)僅保證了主記憶體寫操作。

主記憶體屏障是底層原語,是主記憶體排序的一部分,在不同體系結構下變化很大而不適合推廣。需要認真研讀硬體的手冊以確定主記憶體屏障的辦法。x86指令集中的主記憶體屏障指令是:

lfence (asm), void _mm_lfence (void) 读操作屏障
sfence (asm), void _mm_sfence (void)[1] 写操作屏障
mfence (asm), void _mm_mfence (void)[2] 读写操作屏障

常見的x86/x64,通常使用lock指令字首加上一個空操作來實現,注意當然不能真的是nop指令,但是可以用來實現空操作的指令其實是很多的,比如Linux中採用的

 addl $0, 0 (%esp)

記憶體也提供了另一套語意[3]的主記憶體屏障指令:

  • acquire semantics: 該操作結果可利用要早於代碼中後續的所有操作的結果。
  • release semantics: 該操作結果可利用要晚於代碼中之前的所有操作的結果。
  • fence semantics: acquire與release兩種語意的共同有效。即該操作結果可利用要晚於代碼中之前的所有操作的結果,且該操作結果可利用要早於代碼中後續的所有操作的結果。

Intel Itanium處理器,具有主記憶體屏障mf的指令,具有下述modifiers:

  • acq (acquire)
  • rel (release).

Windows API的主記憶體屏障實現

下述同步函式使用適當的屏障來確保主記憶體有序:

  • 進出臨界區(critical section)的函式
  • 觸發(signaled)同步對象的函式
  • 等待函式(Wait function)
  • 互鎖函式(Interlocked function)

多執行緒編程與主記憶體可見性

多執行緒程式通常使用高層程式設計語言中的同步原語,如Java.NET Framework,或者APIpthreadWindows API。因此一般不需要明確使用主記憶體屏障。

主記憶體可見性問題,主要是高速緩衝記憶體與主記憶體的一致性問題。一個處理器上的執行緒修改了某資料,而在另一處理器上的執行緒可能仍然使用著該資料在專用cache中的老值,這就是可見性出了問題。解決辦法是令該資料為volatile屬性,或者讀該資料之前執行主記憶體屏障。

亂序執行與編譯器重排序最佳化的比較

C與C++語言中,volatile關鍵字意圖允許主記憶體對映的I/O操作。這要求編譯器對此的資料讀寫按照程式中的先後順序執行,不能對volatile主記憶體的讀寫重排序。因此關鍵字volatile並不保證是一個主記憶體屏障。[4]

對於Visual Studio 2003,編譯器保證對volatile的操作是有序的,但是不能保證處理器的亂序執行。因此,可以使用InterlockedCompareExchange或InterlockedExchange函式。

對於Visual Studio 2005及以後版本,編譯器對volatile變數的讀操作使用acquire semantics,對寫操作使用release semantics。

編譯器主記憶體屏障

編譯器會對生成的可執行代碼做一定最佳化,造成亂序執行甚至省略(不執行)。gcc編譯器在遇到內嵌組譯語句

asm volatile("" ::: "memory");

將以此作為一條主記憶體屏障,重排序主記憶體操作。即此語句之前的各種編譯最佳化將不會持續到此語句之後。也可用內建的__sync_synchronize

Microsoft Visual C++的編譯器主記憶體屏障為:

_ReadWriteBarrier() MemoryBarrier()

Intel C++編譯器的主記憶體屏障為:

__memory_barrier()

參考文獻

  1. ^ SFENCE—Store Fence. [2014-01-10]. (原始內容存檔於2019-06-13). 
  2. ^ MFENCE—Memory Fence. [2014-01-10]. (原始內容存檔於2019-09-05). 
  3. ^ MSDN:Synchronization and Multiprocessor Issues. [2016-09-05]. (原始內容存檔於2017-07-04). 
  4. ^ Volatile Considered Harmful - Linux Kernel Documentation. [2014-01-10]. (原始內容存檔於2013-11-02). 

外部連結