【併發】偽共享 —— False Sharing
轉自: http://ifeve.com/falsesharing/
作者:Martin Thompson 譯者:丁一
快取系統中是以快取行(cache line)為單位儲存的。快取行是2的整數冪個連續位元組,一般為32-256個位元組。最常見的快取行大小是64個位元組。當多執行緒修改互相獨立的變數時,如果這些變數共享同一個快取行,就會無意中影響彼此的效能,這就是偽共享。快取行上的寫競爭是執行在SMP系統中並行執行緒實現可伸縮性最重要的限制因素。有人將偽共享描述成無聲的效能殺手,因為從程式碼中很難看清楚是否會出現偽共享。
為了讓可伸縮性與執行緒數呈線性關係,就必須確保不會有兩個執行緒往同一個變數或快取行中寫。兩個執行緒寫同一個變數可以在程式碼中發現。為了確定互相獨立的變數是否共享了同一個快取行,就需要了解記憶體佈局,或找個工具告訴我們。Intel VTune就是這樣一個分析工具。本文中我將解釋Java物件的記憶體佈局以及我們該如何填充快取行以避免偽共享。
圖 1. |
圖1說明了偽共享的問題。在核心1上執行的執行緒想更新變數X,同時核心2上的執行緒想要更新變數Y。不幸的是,這兩個變數在同一個快取行中。每個執行緒都要去競爭快取行的所有權來更新變數。如果核心1獲得了所有權,快取子系統將會使核心2中對應的快取行失效。當核心2獲得了所有權然後執行更新操作,核心1就要使自己對應的快取行失效。這會來來回回的經過L3快取,大大影響了效能。如果互相競爭的核心位於不同的插槽,就要額外橫跨插槽連線,問題可能更加嚴重。
對於HotSpot JVM,所有物件都有兩個字長的物件頭。第一個字是由24位雜湊碼和8位標誌位(如鎖的狀態或作為鎖物件)組成的Mark Word。第二個字是物件所屬類的引用。如果是陣列物件還需要一個額外的字來儲存陣列的長度。每個物件的起始地址都對齊於8位元組以提高效能。因此當封裝物件的時候為了高效率,物件欄位宣告的順序會被重排序成下列基於位元組大小的順序:
- doubles (8) 和 longs (8)
- ints (4) 和 floats (4)
- shorts (2) 和 chars (2)
- booleans (1) 和 bytes (1)
- references (4/8)
- <子類欄位重複上述順序>
為了展示其效能影響,我們啟動幾個執行緒,每個都更新它自己獨立的計數器。計數器是volatile long型別的,所以其它執行緒能看到它們的進展。
01 |
public final class FalseSharing |
02 |
implements Runnable |
03 |
{ |
04 |
public final static int NUM_THREADS
= 4 ; //
change |
05 |
public final static long ITERATIONS
= 500L * 1000L * 1000L; |
06 |
private final int arrayIndex; |
07 |
08 |
private static VolatileLong[]
longs = new VolatileLong[NUM_THREADS]; |
09 |
static |
10 |
{ |
11 |
for ( int i
=
|