內存屏障理解


內存屏障緣由

1. 單處理器下的亂序問題

2. 多處理器下的內存同步問題

舉例:

 

 

 在如圖的這種系統模型中,假設存在如下的內存訪問操作:

 

 由於處理器出於效率而引入的亂序執行(out-of-order execution)和緩存的關系, 對於內存來說, 最后x和y的值可以有如下組合:

 

 

因此,對於在操作系統這一層次編程的程序員來說,他們需要一個內存模型,以協調處理器間正確地使用共享內存,這個模型叫做內存一致性模型(memory consistency model)或簡稱內存模型(memory model)

一種直觀的內存模型叫做順序一致性模型(sequential consistency model), 簡單講,順序一致性模型保證兩件事:

  1. 每個處理器(或線程)的執行順序是按照最后匯編的二進制程序中指令的順序來執行的,
  2. 對於每個處理器(或線程),所有其他處理器(或線程)看到的執行順序跟它的實際執行順序一樣。

這里的順序有三種情況:

  1. 程序順序(program order): 指給定的處理器上,程序最終編譯后的二進制程序中的內存訪問指令的順序,因為編譯器的優化可能會重排源程序中的指令順序。
  2. 執行順序(execution order): 指給定的處理器上,內存訪問指令的實際執行順序。這可能由於處理器的亂序執行而與程序順序不同。
  3. 觀察順序(perceived order): 指給定的處理器上,其觀察到的所有其他處理器的內存訪問指令的實際執行順序。這可能由於處理器的緩存及處理器間內存同步的優化而與執行順序不同

 

何謂內存屏障

上文已經粗略描述了多處理架構下,為了提高並行度,充分挖掘處理器效率的做法會導致的一些與程序員期待的不同的執行結果的情況。本節更詳細地描述這種情況, 即為何順序一致性的模型難以保持的原因。

總的來說,在系統程序員關注的操作系統層面,會重排程序指令執行順序的兩個主要的來源是處理器優化編譯器優化

 

內存屏障的種類

Linux內核實現的屏障種類有以下幾種:

寫屏障(write barriers)

  • 定義: 在寫屏障之前的所有寫操作指令都會在寫屏障之后的所有寫操作指令更早發生。

  • 注意1: 這種順序性是相對這些動作的承接者,即內存來說。也就是說,在一個處理器上加入寫屏障不能保證別的處理器上看到的就是這種順序,也就是觀察順序執行順序無關。

  • 注意2: 寫屏障不保證屏障之前的所有寫操作在屏障指令結束前結束。也就是說,寫屏障序列化了寫操作的發生順序,卻沒保證操作結果發生的序列化。

讀屏障(write barriers)

  • 定義: 在讀屏障之前的所有讀操作指令都會在讀屏障之后的所有讀操作指令更早發生。另外,它還包含后文描述的數據依賴屏障的功能

  • 注意1: 這種順序性是相對這些動作的承接者,即內存來說。也就是說,在一個處理器上加入讀屏障不能保證別的處理器上實際執行的就是這種順序,也就是觀察順序執行順序無關。

  • 注意2: 讀屏障不保證屏障之前的所有讀操作在屏障指令結束前結束。也就是說,讀屏障序列化了讀操作的發生順序,卻沒保證操作結果發生的序列化。

通用屏障(general barriers)

  • 定義: 在通用屏障之前的所有寫和讀操作指令都會在通用屏障之后的所有寫和讀操作指令更早發生。

  • 注意1: 這種順序性是相對這些動作的承接者,即內存來說。也就是說,在一個處理器上加入通用屏障不能保證別的處理器上看到的就是這種順序,也就是觀察順序執行順序無關。

  • 注意2: 通用屏障不保證屏障之前的所有寫和讀操作在屏障指令結束前結束。也就是說,通用屏障序列化了寫和讀操作的發生順序,卻沒保證操作結果發生的序列化。

  • 注意3: 通用屏障是最嚴格的屏障,這也意味着它的低效率。它可以替換在寫屏障或讀屏障出現的地方

數據依賴屏障(data dependency barriers):

volatile關鍵字

voldatile關鍵字特性:

1. 具有“易變性”:  聲明為volatile變量編譯器會強制要求讀內存,相關語句不會直接使用上一條語句對應的的寄存器內容,而是重新從內存中讀取。

2. 具有”不可優化”性:  volatile告訴編譯器,不要對這個變量進行各種激進的優化,甚至將變量直接消除,保證代碼中的指令一定會被執行。

3. 具有“順序性”:  能夠保證Volatile變量間的順序性,編譯器不會進行亂序優化。不過要注意與非volatile變量之間的操作,還是可能被編譯器重排序的。

需要注意的是其含義跟原子操作無關,比如:volatile int a; a++; 其中a++操作實際對應三條匯編指令實現”讀-改-寫“操作(RMW),並非原子的。

為什么要使用 Volatile

Volatile 變量修飾符如果使用恰當的話,它比 synchronized 的使用和執行成本會更低,因為它不會引起線程上下文的切換和調度。

Volatile 的實現原理

那么 Volatile 是如何來保證可見性的呢?在 x86 處理器下通過工具獲取 JIT 編譯器生成的匯編指令來看看對 Volatile 進行寫操作 CPU 會做什么事情。

 

1 0x01a3de1d: movb $0x0,0x1104800(%esi);
2 0x01a3de24: lock addl $0x0,(%esp);

 

有 volatile 變量修飾的共享變量進行寫操作的時候會多第二行匯編代碼,通過查 IA-32 架構軟件開發者手冊可知,lock 前綴的指令在多核處理器下會引發了兩件事情。

  • 將當前處理器緩存行的數據會寫回到系統內存。
  • 這個寫回內存的操作會引起在其他 CPU 里緩存了該內存地址的數據無效。

總結:
volatile的作用是防止編譯器對訪存指令做優化,例如,在一個線程的一段代碼里要定期讀一個變量a,根據讀到的不同值做不同事情,但這個a的修改是在另一個線程里做的,那編譯器可能就認為a沒有被改過從而不是去每次從內存里去讀新的a(把a放在一個臨時寄存器里,每次讀寄存器)。
用volatile關鍵字修飾a的作用就是讓使用a的代碼每次都真正從內存里去讀。volatile的作用僅此而已(沒有保證原子性之類的作用,保證原子性該加鎖還是要加鎖)。

為什么追加 64 字節能夠提高並發編程的效率呢

因為對於英特爾酷睿 i7,酷睿, Atom 和 NetBurst, Core Solo 和 Pentium M 處理器的 L1,L2 或 L3 緩存的高速緩存行是 64 個字節寬,不支持部分填充緩存行,這意味着如果隊列的頭節點和尾節點都不足 64 字節的話,處理器會將它們都讀到同一個高速緩存行中,在多處理器下每個處理器都會緩存同樣的頭尾節點,當一個處理器試圖修改頭接點時會將整個緩存行鎖定,那么在緩存一致性機制的作用下,會導致其他處理器不能訪問自己高速緩存中的尾節點,而隊列的入隊和出隊操作是需要不停修改頭接點和尾節點,所以在多處理器的情況下將會嚴重影響到隊列的入隊和出隊效率。Doug lea 使用追加到 64 字節的方式來填滿高速緩沖區的緩存行,避免頭接點和尾節點加載到同一個緩存行,使得頭尾節點在修改時不會互相鎖定。

 

參考:https://my.oschina.net/u/269082/blog/873612/ 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM