好理解的Java内存虚假共享(False Sharing)性能损耗以及解决方案
虛假共享(False Sharing)也有人翻譯為偽共享
參考?https://en.wikipedia.org/wiki/False_sharing
在計算機科學中,虛假共享是一種性能降低的使用模式,它可能出現在具有由高速緩存機制管理的最小資源塊大小的分布式一致高速緩存的系統中。當系統參與者將定期嘗試訪問,將永遠不會被另一方改變數據,但這些數據共享與數據的高速緩存塊被修改,緩存協議可能迫使一位與會者盡管缺乏邏輯必然性的整個單元重載。高速緩存系統不知道該塊內的活動,并迫使第一個參與者承擔真正共享資源訪問所需的高速緩存系統開銷。
到目前為止,該術語最常見的用法是在現代多處理器?CPU高速緩存中,其中存儲器被緩存在兩個字大小的一些小功率的行中(例如,64個對齊的,連續的字節)。如果兩個處理器對可存儲在一行中的同一存儲器地址區域中的獨立數據進行操作,則系統中的高速緩存一致性機制可能會在每次數據寫入時強制整個線路穿過總線或互連,從而除了浪費系統帶寬之外還會導致內存停頓?。虛假共享是自動同步的緩存協議的固有工件,也可以存在于分布式文件系統或數據庫等環境中,但目前的流行僅限于RAM緩存。
示例
struct foo {int x;int y;
};static struct foo f;/* The two following functions are running concurrently: */int sum_a(void)
{int s = 0;for (int i = 0; i < 1000000; ++i)s += f.x;return s;
}void inc_b(void)
{for (int i = 0; i < 1000000; ++i)++f.y;
}
在這里,sum_a可能需要不斷地從主存儲器(而不是從緩存)重新讀取x,即使inc_b并發修改y是無關緊要的。
如果你還是不能理解虛假共享不要緊看下面的例子
理解虛假分享
為了更好地理解這一點,我們假設一個假設的情況:
有三位畫家。每個人都有他自己的木板,他們在上面繪畫,每個板有三個部門,分別是1區,2區和3區。
畫家只能畫出這三個部門中的一個。當畫家描繪他的木板的一個部分時,另外兩個板也必須改變以反映第一個畫家所做的事情。
這里的木板類似于緩存塊,畫家類似于并發線程,繪畫類似于寫入活動。
請記住,此更新在邏輯上是不必要的,因為每個畫家使用的分區不與其他畫家使用的分區相交。可以做的是在所有畫家完成繪畫之后,最后可以更新木板。但這不是我們的計算機架構的工作方式。這是因為管理高速緩存機制的組件不知道實際更新了高速緩存塊的哪個分區。它標記整個塊為臟。強制內存更新以維持緩存一致性。與高速緩存塊中的寫入活動相比,這是非常昂貴的計算。
只有當寫入進程和兩個并行線程具有交叉緩存塊時才會出現此問題。現在解決此問題的唯一方法是確保兩個并行線程具有不同的緩存塊。
?
參考:虛假分享
要實現線程數量的線性可伸縮性,我們必須確保沒有兩個線程同時寫入同一個變量或緩存行。可以在代碼級別跟蹤寫入同一變量的兩個線程。為了能夠知道自變量是否共享相同的緩存行,我們需要知道內存布局,或者我們可以使用工具告訴我們。英特爾VTune就是這樣一個分析工具。下面,將解釋如何為Java對象布置內存以及如何填充緩存行以避免錯誤共享。
上圖演示虛假共享的問題。
在核心1(Core1)上運行的線程想要更??新變量X,而核心2(Core2)上的線程想要更??新變量Y。
不幸的是,這兩個變量位于同一緩存行中。每個線程都將競爭對緩存行的所有權,以便可以更新。如果核心1獲得所有權,那么緩存子系統將需要使核心2的相應緩存行置為無效。當Core 2獲得所有權并執行其更新時,將告知核心1使其緩存行的副本無效。這將通過L3緩存來回乒乓,會極大的影響性能。如果競爭核心在不同的套接字上并且還必須跨越套接字互連,那么將進一步加劇性能問題。
?
Java內存布局
對于基于Hotspot的JVM比如現在的OpenJDK和OracleJDK,所有對象都有一個2個字的header。首先是“標記(mark)”字,其由用于散列碼的24位和用于諸如鎖定狀態的標志的8位組成,或者它可以被交換用于鎖定對象。第二個是對象類的引用。數組有一個額外的單詞,用于表示數組的大小。為了提高性能,每個對象都與8字節的粒度邊界對齊。因此,為了在打包時有效,根據大小(以字節為單位)將對象字段從聲明順序重新排序為以下順序:
doubles (8) and longs (8)
ints (4) and floats (4)
shorts (2) and chars (2)
booleans (1) and bytes (1)
references (4/8)
<repeat for sub-class fields> 重復子類字段
有了這些知識,我們可以在7個長度的任何字段之間填充緩存行。為了顯示性能影響,讓我們花幾個線程來更新自己獨立的計數器。這些計數器將長期波動,可以看到它們的比較數據。
package linuxstyle.blog.csdn.net;public final class FalseSharing implements Runnable {public final static long ITERATIONS = 500L * 1000L * 1000L;public final static int NUM_THREADS = 4; // changeprivate static VolatileLong[] longs = new VolatileLong[NUM_THREADS];static {for (int i = 0; i < longs.length; i++) {longs[i] = new VolatileLong();}}private final int arrayIndex;public FalseSharing(final int arrayIndex) {this.arrayIndex = arrayIndex;}public static void main(final String[] args) throws Exception {final long start = System.nanoTime();runTest();System.out.println("duration = " + (System.nanoTime() - start));}private static void runTest() throws InterruptedException {Thread[] threads = new Thread[NUM_THREADS];for (int i = 0; i < threads.length; i++) {threads[i] = new Thread(new FalseSharing(i));}for (Thread t : threads) {t.start();}for (Thread t : threads) {t.join();}}public void run() {long i = ITERATIONS + 1;while (0 != --i) {longs[arrayIndex].value = i;}}public final static class VolatileLong {public long p1, p2, p3, p4, p5, p6; // comment outpublic volatile long value = 0L;}
}
輸出如下:
結果
運行上面的代碼,同時增加線程數并添加/刪除緩存行填充,得到如下圖所示的結果。這是測量4核測試運行的持續時間。?
通過增加完成測試所需的執行時間可以清楚地看出錯誤共享的影響。如果沒有緩存行爭用,我們就可以通過線程實現近似線性擴展。
這不是一個完美的測試,因為我們無法確定VolatileLongs將在內存中的位置。它們是獨立的對象。但是經驗表明,同時分配的對象往往位于同一位置。
?
需要注意的是上面的解決辦法是有爭議的參考:知道你的Java對象內存布局
理論上,理論和實踐是相同的
這是幾年前的一篇優秀文章,它告訴大家Java應該如何布局你的對象,總結一下:
- 對象在內存中對齊8個字節(如果A%K == 0,則地址A為K對齊)
- 所有字段都是類型對齊的(long / double是8對齊,整數/ float 4,short / char 2)
- 字段按其大小的順序打包,除了最后的引用
- 類字段永遠不會混合,所以如果B擴展A,B類的對象將首先在A的字段中布局在內存中,然后是B的
- 子類字段以4字節對齊開始
- 如果類的第一個字段是long / double并且類起始點(在標題之后,或者在super之后)不是8對齊,則可以交換較小的字段以填充4字節間隙。
JVM不僅僅按照你告訴它的順序依次對你的字段進行plok的原因也在文章中討論,總結如下:
- 未對齊訪問是不好的,因此JVM可以避免錯誤的布局(對內存的未對齊訪問會導致各種不良副作用,包括在某些體系結構上崩潰您的進程)
- 字段的樸素布局會浪費內存,JVM重新排序字段以改善對象的整體大小
- JVM實現要求類型具有一致的布局,因此需要子類規則
那么......很好的明確規則,可能會出錯?
https://gist.github.com/nitsanw/5594570#file-gistfile1-java
首先,規則不是JLS的一部分,它們只是實現細節。如果您閱讀Martin Thompson關于虛假共享的文章,??您會注意到T先生有一個錯誤共享的解決方案,該解決方案適用于JDK 6,但不再適用于JDK 7.以下是兩個版本。
下面是避免在JDK 6/7上進行錯誤共享:
// No false sharing on 6, but happens on 7
public final static class VolatileLong
{public volatile long value = 0L;public long p1, p2, p3, p4, p5, p6;
}
// No false sharing on 6 or 7
public static class PaddedAtomicLong extends AtomicLong
{public volatile long p1, p2, p3, p4, p5, p6 = 7L;
}
事實證明,JVM改變了它對6到7之間的字段進行排序的方式,這足以打破這個咒語。公平地說,沒有上面規定的規則要求字段順序與它們被定義的順序相關聯,但是......它需要擔心并且它可以讓你絆倒。
正如上述規則在我的腦海中仍然是新鮮的,LMAX?開源的Disruptor發布了Coalescing Ring Buffer。我仔細閱讀了代碼并發現以下內容:
public final class CoalescingRingBuffer<K, V> implements CoalescingBuffer<K, V> {private volatile long nextWrite = 1; // <-- producer access (my comment)private volatile long lastCleaned = 0; // <-- producer access (my comment)private volatile long rejectionCount = 0;private final K[] keys;private final AtomicReferenceArray<V> values;private final K nonCollapsibleKey = (K) new Object();private final int mask;private final int capacity;private volatile long nextRead = 1; // <-- consumer access (my comment)private volatile long lastRead = 0; // <-- consumer access (my comment)...
}
在介紹CoalescingRingBuffer的博客文章中找到了Nick Zeeb,??并提出了擔憂,即生產者/消費者訪問的字段可能會遭受錯誤的共享,Nick的回復:
試圖對字段進行排序,以便最大限度地減少錯誤共享的風險。Java 7可以重新排序字段。使用Martin Thompson的PaddedAtomicLong進行了性能測試,但沒有在Java 7上獲得性能提升。
尼克很聰明,并不是在這里引用這些用來來批評他。引用他來表明這是令人困惑的東西(所以在某種程度上,我引用他來安慰自己與其他同樣困惑的專業人士的公司)。我們怎么知道?這是我和尼克交談后想到的一種方式:
public class FalseSharingTest {@Testpublic void test() throws NoSuchFieldException, SecurityException{long nextWriteOffset = UnsafeAccess.unsafe.objectFieldOffset(CoalescingRingBuffer.class.getDeclaredField("nextWrite"));long lastReadOffset = UnsafeAccess.unsafe.objectFieldOffset(CoalescingRingBuffer.class.getDeclaredField("lastRead"));assertTrue(Math.abs(nextWriteOffset - lastReadOffset) >= 64);}
}
使用Unsafe我可以從對象引用中獲取字段偏移量,如果2個字段小于高速緩存行,則它們可能遭受錯誤共享(取決于內存中的結束位置)。當然,這是驗證事物的一種hackish方式,但它可以成為您構建的一部分。
熱門
大約在同一時間LMAX發布了CoalescingRingBuffer,Gil Tene(Azul的CTO)發布了HdrHistogram。現在Gil非常認真,非常聰明,并且比大多數人更了解JVM(這是他的InfoQ演講,觀看它)所以我很想看看他的代碼。你知道什么,一堆熱門領域:
public abstract class AbstractHistogram implements Serializable {// "Cold" accessed fields. Not used in the recording code path:long highestTrackableValue;int numberOfSignificantValueDigits;int bucketCount;int subBucketCount;int countsArrayLength;HistogramData histogramData;// Bunch "Hot" accessed fields (used in the the value recording code path) here, near the end, so// that they will have a good chance of ending up in the same cache line as the counts array reference// field that subclass implementations will add.int subBucketHalfCountMagnitude;int subBucketHalfCount;long subBucketMask;...
}
Gil在這里做的很好,他試圖讓相關領域在內存中擠在一起,這將提高他們在同一緩存行上結束的可能性,從而為CPU節省潛在的緩存。可悲的是,JVM還有其他計劃......?
所以這里有另一個工具可以幫助你理解你的內存布局,以便添加到你的工具帶中:Java Object Layout??我偶然碰到了它,而不是一直想著內存布局。
注意histogramData如何跳轉到botton并且subBucketMask被移到頂部,打破了我們的熱門束。解決方案是丑陋但有效的,將所有字段移動到另一個毫無意義的父類:
abstract class AbstractHistogramColdFields implements Serializable {// "Cold" accessed fields. Not used in the recording code path:long highestTrackableValue;int numberOfSignificantValueDigits;int bucketCount;int subBucketCount;int countsArrayLength;HistogramData histogramData;
}
public abstract class AbstractHistogram extends AbstractHistogramColdFields {// Bunch "Hot" accessed fields (used in the the value recording code path) here, near the end, so// that they will have a good chance of ending up in the same cache line as the counts array reference// field that subclass implementations will add.int subBucketHalfCountMagnitude;int subBucketHalfCount;long subBucketMask;...
}
優秀的JOL現已在OpenJDK下發布。它甚至比以前更好,并支持許多時髦的功能。
http://openjdk.java.net/projects/code-tools/jol/
代碼工具:jol
JOL(Java Object Layout)是分析JVM中對象布局方案的微型工具箱。這些工具大量使用Unsafe,JVMTI和Serviceability Agent(SA)來解碼實際的?對象布局,占用空間和引用。這使得JOL比依賴堆轉儲,規范假設等的其他工具更準確。
?
參考:
- 易于理解虛假分享
- C ++今日博客,虛假分享再次點擊!
- Dobbs博士的文章:消除虛假分享
- 在嘗試消除Java中的錯誤共享時要小心
- Bolosky,WJ和Scott,ML 1993.?虛假共享及其對共享內存性能的影響
總結
以上是生活随笔為你收集整理的好理解的Java内存虚假共享(False Sharing)性能损耗以及解决方案的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 家庭用的自来水水管一般多少钱一米?
- 下一篇: 什么时候10086也开始发诈骗短信了 这