709
技術社區[雲棲]
Java 7與偽共享的新仇舊恨
在我前一篇有關偽共享的博文中, 我提到了可以加入閑置的long字段來填充緩存行來避免偽共享。但是看起來Java 7變得更加智慧了,它淘汰或者是重新排列了無用的字段,這樣我們之前的辦法在Java 7下就不奏效了,但是偽共享依然會發生。我在不同的平台上實驗了一些列不同的方案,並且最終發現下麵的代碼是最可靠的。(譯者注:下麵的是最終版本,馬丁 在大家的幫助下修改了幾次代碼)
01 import java.util.concurrent.atomic.AtomicLong;
02
03 public final class FalseSharing
04 implements Runnable
05 {
06 public final static int NUM_THREADS = 4; // change
07 public final static long ITERATIONS = 500L * 1000L * 1000L;
08 private final int arrayIndex;
09
10 private static PaddedAtomicLong[] longs = new PaddedAtomicLong[NUM_THREADS];
11 static
12 {
13 for (int i = 0; i < longs.length; i++)
14 {
15 longs[i] = new PaddedAtomicLong();
16 }
17 }
18
19 public FalseSharing(final int arrayIndex)
20 {
21 this.arrayIndex = arrayIndex;
22 }
23
24 public static void main(final String[] args) throws Exception
25 {
26 final long start = System.nanoTime();
27 runTest();
28 System.out.println("duration = " + (System.nanoTime() - start));
29 }
30
31 private static void runTest() throws InterruptedException
32 {
33 Thread[] threads = new Thread[NUM_THREADS];
34
35 for (int i = 0; i < threads.length; i++)
36 {
37 threads[i] = new Thread(new FalseSharing(i));
38 }
39
40 for (Thread t : threads)
41 {
42 t.start();
43 }
44
45 for (Thread t : threads)
46 {
47 t.join();
48 }
49 }
50
51 public void run()
52 {
53 long i = ITERATIONS + 1;
54 while (0 != --i)
55 {
56 longs[arrayIndex].set(i);
57 }
58 }
59
60 // 這段代碼的來曆可以看4樓的回複
61 public static long sumPaddingToPreventOptimisation(final int index)
62 {
63 PaddedAtomicLong v = longs[index];
64 return v.p1 + v.p2 + v.p3 + v.p4 + v.p5 + v.p6;
65 }
66
67 public static class PaddedAtomicLong extends AtomicLong
68 {
69 public volatile long p1, p2, p3, p4, p5, p6 = 7L;
70 }
71 }
|
用以上這種辦法我獲得了和上一篇博客裏提到的相近的性能,讀者可以把PaddedAtomicLong裏麵那行填充物注釋掉再跑測試看看效果。
我想我們大家都有權去跟Oracle投訴,讓他們在JDK裏默認加入緩存行對齊的函數或者是被填充好的原子類型,這和其他一些底層改變會讓Java 成為一門真真正正的並發編程語言。我們一直以來不斷的在聽到他們講多核時代正在到來,但是我要說的是在這方麵Java需要快點趕上來。
———————————————–
(譯者注:博文後麵的評論和交流也很精彩,也講述了這段示例代碼的進化過程,一起翻譯出來:)
1樓:Ashwin Jayaprakash
在前一篇博文中你創建了一個數組來放VolatileLong,這次你又用一個數組放AtomicLongArray(譯者注:此處我覺得他可能是寫錯了,應該是說AtomicLong吧)。
但是如何能保證AtomicLongArray或VolatileLong會被緊挨著分配在內存中?
那麼,就算你在一個循環中創建他們,並且很幸運的,他們獲得了連續的內存空間,但是依然無法保證這四個實例會在堆空間裏緊挨著。如果他們被分布在JVM的舊生代堆裏並且沒有被壓實的話,直到一次主要GC壓實舊生代之前,重新分配填充是沒必要的,因為他們在堆中是分散的。
所以你最好對讀者說明,我們無法控製JVM如何在堆中對這些實例分配內存。(譯者注:沒辦法,要精確控製內存來保證性能的話就不要用Java了,要不直接用C好了)
———————————————–
2樓:馬丁
你大體上說的是對的,Ashwin,我們無法保證如何在堆空間中放置Java對象,這是偽共享問題發生的根源。如果你有一些跨線程的指針或者計數
器,那麼確保他們在不同的緩存行中是非常重要的,否則的話程序就無法按CPU的核數擴展。填充的根本意義在於保護這些跨線程的指針和計數器,以確保他們在
不同的緩存行中。
這個是有意義的吧?
———————————————–
3樓:Ashwin Jayaprakash
嗯,有道理。那你可不可以創建一個大的AtomicLongArray,然後讓不同的線程去更新第8,16,32個元素呢?(譯者注:也算是消除競
爭的一個辦法,但是既然完全沒有競爭還要多線程做什麼?)而不是搞四個AtomicLongArray,而每個線程都去競爭訪問同一個數組元素。
謝謝馬丁花時間寫了這麼多。
———————————————–
4樓:馬丁
如果我可以提前知道更多的業務邏輯那麼你說的方式是可行的。但通常情況下在設計一個大型係統的時候,我們無法提前知道很多事情,或者我們要為其他的應用創造一個通用的類庫。
我很難為很多不同的上下文場景寫一個足夠小巧簡單的示例,而我上麵寫的示例是為了說明當偽共享發生的時候有多糟糕。如果你在你的數據結構中做了填充,那麼
你就不必擔心他們在內存中如何分配。我們用一個更好的方案來替代AtomicLong,並且你可以使用AtomicLong的所有常規方法:
static class PaddedAtomicLong extends AtomicLong
{
public volatile long p1, p2, p3, p4, p5, p6, p7 = 7L;
}
我是多希望Java委員會可以認識到這個問題的嚴重性,並且在JDK裏加入對緩存行對齊和填充的基礎方法。這是在Disruptor中有關性能BUG的最大根源。
我也根據以上的反饋更新了文章。
———————————————–
5樓:Gil Tene
馬丁,我很同意你的觀點,如果我們有一種辦法可以指定某個字段占有獨自的緩存行,並且讓JVM自動處理如何在對象布局上的正確填充,那這個世界會和諧的多。你搞的這個人造填充將會是很美好的一個事情,但是你也知道,實際上的對象布局情況要取決於JVM的特定實現。
我是一個偏執狂,我給你的填充方案裏加了一些東西,使那些個用於填充的字段很難被JVM優化掉。一個耍小聰明的JVM還是會把你用於填充的P1-P7的字
段優化掉,原理是這樣滴:PaddedAtomicLong類如果隻對final的FalseSharing類可見(就是說
PaddedAtomicLong不能再被繼承了)。這樣一來編譯器就會“知道”它正在審視的是所有可以看到這個填充字段的代碼,這樣就可以證明沒有行為
依賴於p1到p7這些字段。那麼“聰明”的JVM會把上麵這些絲毫不占地方的字段統統優化掉。
那麼針對這樣的情況,你可以巧妙的讓PaddedAtomicLong類在FalseSharing類之外可見,比如直接加一個依賴於p1到p7的公開的訪問函數,並且這個函數在理論上可以被外界訪問到。
———————————————–
6樓:馬丁
我根據Gil的反饋做了修改。
———————————————–
7樓:Stanimir Simeonoff
直接用一個數組並且把元素放在中間的位置上(或者直接用bytebuffer,而你卻為了這個寫了這麼一大篇),Java是不會重排他們的,我就是這樣來消除偽共享的。
———————————————–
8樓:馬丁
我以前經常像你這麼幹,比如搞一個長度是15的數組,把元素放在正中間,但是,如果我們需要volatile這個語意就行不通了。對於你的情況來說,你隻
需要用AtomicLongArray或者類似的。根據我的測量,在一個算法中,這種間接引用(譯者注:原詞是indirection,我理解也許是指間
接引用,即不是直接使用數組,而是使用AtomicLongArray這種包裝過的數組)和邊界檢查的消耗是顯著的。
據我所知,一些人建議加入@Contened注解來標記一個字段,讓這個被標記的字段擁有獨立的緩存行,我希望這個快點到來。
8.1樓:John
你好,馬丁,我看到在Disruptor當前的版本中Sequence類用的是unsafe.compareAndSwapLong(..)來更新第七個下標的long。
為什麼不數組的長度不是15或者是其他的數值?如果長度是15的話會把2級緩存的緩存行也填充掉麼?
謝謝。
8.2樓:馬丁
因為用7個下標保證了會有56個字節填充在數值的任何一邊,56字節的填充+8字節的long數值正好裝進一行64字節的緩存行。
———————————————–
9樓:Stanimir Simeonoff
是的,馬丁,我指的就是AtomicLongArray,如果你不想為間接引用和邊界檢查買單,Unsafe 是一個選項(甚至總是這樣)。
———————————————–
10樓:Mohan Radhakrishnan
哪裏有一些簡單硬件說明書是講述緩存行的關鍵概念麼?我想找一些插圖什麼的來理解核心和緩存直接如何交互造成了偽共享。
———————————————–
11樓:馬丁
你可以參照下麵這個PDF的第3和第4章:
https://img.delivery.net/cm50content/intel/ProductLibrary/100412_Parallel_Programming_02.pdf
———————————————–
12樓:ying
你的博客太NB了,多謝馬丁,我關於填充有兩個疑問:
1.long占8字節,對象引用占16字節,但是這個實現是
1 |
public final static class VolatileLong // 16byte</pre>
|
2 |
{ |
3 |
public volatile long value = 0L; // 8 byte
|
4 |
public long p1, p2, p3, p4, p5, p6; // 6*8 = 48byte
|
5 |
} |
看起來好像是72個字節啊。
2.你是發現這個問題的?是去查匯編代碼嗎?
12.1樓:馬丁
我不希望在緩存行中的標記字在取出鎖或者垃圾回收器在老化對象的時候被修改。
就算默認啟用64位模式的壓縮指針,它還是會包含類指針在對象的頭部。
https://wikis.oracle.com/display/HotSpotInternals/CompressedOops
這個偽共享的問題我是在多年前發現的,但是我為一個應用做性能測試,發現性能時高時低,追查原因下去發現是偽共享問題。
12.2樓:ying
那你是如何縮小問題的範圍最後發現問題的呢?需要深入分析匯編代碼麼?
11.3樓:馬丁
匯編代碼是不會顯示出問題的,你需要去追查為什麼CPU的2級緩存總是不命中,追查下去就知道了。
———————————————–
13樓:Joachim
關於@Contended注解的提案在這裏:
https://mail.openjdk.java.net/pipermail/hotspot-dev/2012-November/007309.html
牛文啊,讚!
文章轉自 並發編程網-ifeve.com
最後更新:2017-05-23 10:02:34
上一篇:
False Sharing && Java 7
下一篇:
RecyclerView Part 2:選擇模式
FB不僅僅是兩三天,SOA同樣也是,邀您加入Java2SOA革命大軍,贏限量珍藏版紀念T恤
使用SQL Server Profiler跟蹤數據庫
1682億是“人類科技史上最大規模的一次機器智能應用”,又將會呈現出多少技術?
一個使用FFmpeg庫讀取3gp視頻的例子-Android中使用FFmpeg媒體庫(三)
mac下的eclipse沒有GBK編碼
阿裏數據庫內核月報:2016年08月
白帽子小A的故事:《網安法》時代,挖漏洞安全姿勢指南
WCF技術剖析之六:為什麼在基於ASP.NET應用寄宿(Hosting)下配置的BaseAddress無效
盤點中國第四次工業革命物聯網的數字亮點
阿裏雲關係型數據庫RDS幸運券領取及使用