好理解的Java内存虚假共享(False Sharing)性能损耗以及解决方案
虛假共享(False Sharing)也有人翻譯為偽共享
參考?https://en.wikipedia.org/wiki/False_sharing
在計(jì)算機(jī)科學(xué)中,虛假共享是一種性能降低的使用模式,它可能出現(xiàn)在具有由高速緩存機(jī)制管理的最小資源塊大小的分布式一致高速緩存的系統(tǒng)中。當(dāng)系統(tǒng)參與者將定期嘗試訪問,將永遠(yuǎn)不會(huì)被另一方改變數(shù)據(jù),但這些數(shù)據(jù)共享與數(shù)據(jù)的高速緩存塊被修改,緩存協(xié)議可能迫使一位與會(huì)者盡管缺乏邏輯必然性的整個(gè)單元重載。高速緩存系統(tǒng)不知道該塊內(nèi)的活動(dòng),并迫使第一個(gè)參與者承擔(dān)真正共享資源訪問所需的高速緩存系統(tǒng)開銷。
到目前為止,該術(shù)語最常見的用法是在現(xiàn)代多處理器?CPU高速緩存中,其中存儲(chǔ)器被緩存在兩個(gè)字大小的一些小功率的行中(例如,64個(gè)對(duì)齊的,連續(xù)的字節(jié))。如果兩個(gè)處理器對(duì)可存儲(chǔ)在一行中的同一存儲(chǔ)器地址區(qū)域中的獨(dú)立數(shù)據(jù)進(jìn)行操作,則系統(tǒng)中的高速緩存一致性機(jī)制可能會(huì)在每次數(shù)據(jù)寫入時(shí)強(qiáng)制整個(gè)線路穿過總線或互連,從而除了浪費(fèi)系統(tǒng)帶寬之外還會(huì)導(dǎo)致內(nèi)存停頓?。虛假共享是自動(dòng)同步的緩存協(xié)議的固有工件,也可以存在于分布式文件系統(tǒng)或數(shù)據(jù)庫(kù)等環(huán)境中,但目前的流行僅限于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可能需要不斷地從主存儲(chǔ)器(而不是從緩存)重新讀取x,即使inc_b并發(fā)修改y是無關(guān)緊要的。
如果你還是不能理解虛假共享不要緊看下面的例子
理解虛假分享
為了更好地理解這一點(diǎn),我們假設(shè)一個(gè)假設(shè)的情況:
有三位畫家。每個(gè)人都有他自己的木板,他們?cè)谏厦胬L畫,每個(gè)板有三個(gè)部門,分別是1區(qū),2區(qū)和3區(qū)。
畫家只能畫出這三個(gè)部門中的一個(gè)。當(dāng)畫家描繪他的木板的一個(gè)部分時(shí),另外兩個(gè)板也必須改變以反映第一個(gè)畫家所做的事情。
這里的木板類似于緩存塊,畫家類似于并發(fā)線程,繪畫類似于寫入活動(dòng)。
請(qǐng)記住,此更新在邏輯上是不必要的,因?yàn)槊總€(gè)畫家使用的分區(qū)不與其他畫家使用的分區(qū)相交。可以做的是在所有畫家完成繪畫之后,最后可以更新木板。但這不是我們的計(jì)算機(jī)架構(gòu)的工作方式。這是因?yàn)楣芾砀咚倬彺鏅C(jī)制的組件不知道實(shí)際更新了高速緩存塊的哪個(gè)分區(qū)。它標(biāo)記整個(gè)塊為臟。強(qiáng)制內(nèi)存更新以維持緩存一致性。與高速緩存塊中的寫入活動(dòng)相比,這是非常昂貴的計(jì)算。
只有當(dāng)寫入進(jìn)程和兩個(gè)并行線程具有交叉緩存塊時(shí)才會(huì)出現(xiàn)此問題。現(xiàn)在解決此問題的唯一方法是確保兩個(gè)并行線程具有不同的緩存塊。
?
參考:虛假分享
要實(shí)現(xiàn)線程數(shù)量的線性可伸縮性,我們必須確保沒有兩個(gè)線程同時(shí)寫入同一個(gè)變量或緩存行。可以在代碼級(jí)別跟蹤寫入同一變量的兩個(gè)線程。為了能夠知道自變量是否共享相同的緩存行,我們需要知道內(nèi)存布局,或者我們可以使用工具告訴我們。英特爾VTune就是這樣一個(gè)分析工具。下面,將解釋如何為Java對(duì)象布置內(nèi)存以及如何填充緩存行以避免錯(cuò)誤共享。
上圖演示虛假共享的問題。
在核心1(Core1)上運(yùn)行的線程想要更??新變量X,而核心2(Core2)上的線程想要更??新變量Y。
不幸的是,這兩個(gè)變量位于同一緩存行中。每個(gè)線程都將競(jìng)爭(zhēng)對(duì)緩存行的所有權(quán),以便可以更新。如果核心1獲得所有權(quán),那么緩存子系統(tǒng)將需要使核心2的相應(yīng)緩存行置為無效。當(dāng)Core 2獲得所有權(quán)并執(zhí)行其更新時(shí),將告知核心1使其緩存行的副本無效。這將通過L3緩存來回乒乓,會(huì)極大的影響性能。如果競(jìng)爭(zhēng)核心在不同的套接字上并且還必須跨越套接字互連,那么將進(jìn)一步加劇性能問題。
?
Java內(nèi)存布局
對(duì)于基于Hotspot的JVM比如現(xiàn)在的OpenJDK和OracleJDK,所有對(duì)象都有一個(gè)2個(gè)字的header。首先是“標(biāo)記(mark)”字,其由用于散列碼的24位和用于諸如鎖定狀態(tài)的標(biāo)志的8位組成,或者它可以被交換用于鎖定對(duì)象。第二個(gè)是對(duì)象類的引用。數(shù)組有一個(gè)額外的單詞,用于表示數(shù)組的大小。為了提高性能,每個(gè)對(duì)象都與8字節(jié)的粒度邊界對(duì)齊。因此,為了在打包時(shí)有效,根據(jù)大小(以字節(jié)為單位)將對(duì)象字段從聲明順序重新排序?yàn)橐韵马樞?#xff1a;
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> 重復(fù)子類字段
有了這些知識(shí),我們可以在7個(gè)長(zhǎng)度的任何字段之間填充緩存行。為了顯示性能影響,讓我們花幾個(gè)線程來更新自己獨(dú)立的計(jì)數(shù)器。這些計(jì)數(shù)器將長(zhǎng)期波動(dòng),可以看到它們的比較數(shù)據(jù)。
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;}
}
輸出如下:
結(jié)果
運(yùn)行上面的代碼,同時(shí)增加線程數(shù)并添加/刪除緩存行填充,得到如下圖所示的結(jié)果。這是測(cè)量4核測(cè)試運(yùn)行的持續(xù)時(shí)間。?
通過增加完成測(cè)試所需的執(zhí)行時(shí)間可以清楚地看出錯(cuò)誤共享的影響。如果沒有緩存行爭(zhēng)用,我們就可以通過線程實(shí)現(xiàn)近似線性擴(kuò)展。
這不是一個(gè)完美的測(cè)試,因?yàn)槲覀儫o法確定VolatileLongs將在內(nèi)存中的位置。它們是獨(dú)立的對(duì)象。但是經(jīng)驗(yàn)表明,同時(shí)分配的對(duì)象往往位于同一位置。
?
需要注意的是上面的解決辦法是有爭(zhēng)議的參考:知道你的Java對(duì)象內(nèi)存布局
理論上,理論和實(shí)踐是相同的
這是幾年前的一篇優(yōu)秀文章,它告訴大家Java應(yīng)該如何布局你的對(duì)象,總結(jié)一下:
- 對(duì)象在內(nèi)存中對(duì)齊8個(gè)字節(jié)(如果A%K == 0,則地址A為K對(duì)齊)
- 所有字段都是類型對(duì)齊的(long / double是8對(duì)齊,整數(shù)/ float 4,short / char 2)
- 字段按其大小的順序打包,除了最后的引用
- 類字段永遠(yuǎn)不會(huì)混合,所以如果B擴(kuò)展A,B類的對(duì)象將首先在A的字段中布局在內(nèi)存中,然后是B的
- 子類字段以4字節(jié)對(duì)齊開始
- 如果類的第一個(gè)字段是long / double并且類起始點(diǎn)(在標(biāo)題之后,或者在super之后)不是8對(duì)齊,則可以交換較小的字段以填充4字節(jié)間隙。
JVM不僅僅按照你告訴它的順序依次對(duì)你的字段進(jìn)行plok的原因也在文章中討論,總結(jié)如下:
- 未對(duì)齊訪問是不好的,因此JVM可以避免錯(cuò)誤的布局(對(duì)內(nèi)存的未對(duì)齊訪問會(huì)導(dǎo)致各種不良副作用,包括在某些體系結(jié)構(gòu)上崩潰您的進(jìn)程)
- 字段的樸素布局會(huì)浪費(fèi)內(nèi)存,JVM重新排序字段以改善對(duì)象的整體大小
- JVM實(shí)現(xiàn)要求類型具有一致的布局,因此需要子類規(guī)則
那么......很好的明確規(guī)則,可能會(huì)出錯(cuò)?
https://gist.github.com/nitsanw/5594570#file-gistfile1-java
首先,規(guī)則不是JLS的一部分,它們只是實(shí)現(xiàn)細(xì)節(jié)。如果您閱讀Martin Thompson關(guān)于虛假共享的文章,??您會(huì)注意到T先生有一個(gè)錯(cuò)誤共享的解決方案,該解決方案適用于JDK 6,但不再適用于JDK 7.以下是兩個(gè)版本。
下面是避免在JDK 6/7上進(jìn)行錯(cuò)誤共享:
// 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;
}
事實(shí)證明,JVM改變了它對(duì)6到7之間的字段進(jìn)行排序的方式,這足以打破這個(gè)咒語。公平地說,沒有上面規(guī)定的規(guī)則要求字段順序與它們被定義的順序相關(guān)聯(lián),但是......它需要擔(dān)心并且它可以讓你絆倒。
正如上述規(guī)則在我的腦海中仍然是新鮮的,LMAX?開源的Disruptor發(fā)布了Coalescing Ring Buffer。我仔細(xì)閱讀了代碼并發(fā)現(xiàn)以下內(nèi)容:
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,??并提出了擔(dān)憂,即生產(chǎn)者/消費(fèi)者訪問的字段可能會(huì)遭受錯(cuò)誤的共享,Nick的回復(fù):
試圖對(duì)字段進(jìn)行排序,以便最大限度地減少錯(cuò)誤共享的風(fēng)險(xiǎn)。Java 7可以重新排序字段。使用Martin Thompson的PaddedAtomicLong進(jìn)行了性能測(cè)試,但沒有在Java 7上獲得性能提升。
尼克很聰明,并不是在這里引用這些用來來批評(píng)他。引用他來表明這是令人困惑的東西(所以在某種程度上,我引用他來安慰自己與其他同樣困惑的專業(yè)人士的公司)。我們?cè)趺粗?#xff1f;這是我和尼克交談后想到的一種方式:
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我可以從對(duì)象引用中獲取字段偏移量,如果2個(gè)字段小于高速緩存行,則它們可能遭受錯(cuò)誤共享(取決于內(nèi)存中的結(jié)束位置)。當(dāng)然,這是驗(yàn)證事物的一種hackish方式,但它可以成為您構(gòu)建的一部分。
熱門
大約在同一時(shí)間LMAX發(fā)布了CoalescingRingBuffer,Gil Tene(Azul的CTO)發(fā)布了HdrHistogram。現(xiàn)在Gil非常認(rèn)真,非常聰明,并且比大多數(shù)人更了解JVM(這是他的InfoQ演講,觀看它)所以我很想看看他的代碼。你知道什么,一堆熱門領(lǐng)域:
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在這里做的很好,他試圖讓相關(guān)領(lǐng)域在內(nèi)存中擠在一起,這將提高他們?cè)谕痪彺嫘猩辖Y(jié)束的可能性,從而為CPU節(jié)省潛在的緩存。可悲的是,JVM還有其他計(jì)劃......?
所以這里有另一個(gè)工具可以幫助你理解你的內(nèi)存布局,以便添加到你的工具帶中:Java Object Layout??我偶然碰到了它,而不是一直想著內(nèi)存布局。
注意histogramData如何跳轉(zhuǎn)到botton并且subBucketMask被移到頂部,打破了我們的熱門束。解決方案是丑陋但有效的,將所有字段移動(dòng)到另一個(gè)毫無意義的父類:
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;...
}
優(yōu)秀的JOL現(xiàn)已在OpenJDK下發(fā)布。它甚至比以前更好,并支持許多時(shí)髦的功能。
http://openjdk.java.net/projects/code-tools/jol/
代碼工具:jol
JOL(Java Object Layout)是分析JVM中對(duì)象布局方案的微型工具箱。這些工具大量使用Unsafe,JVMTI和Serviceability Agent(SA)來解碼實(shí)際的?對(duì)象布局,占用空間和引用。這使得JOL比依賴堆轉(zhuǎn)儲(chǔ),規(guī)范假設(shè)等的其他工具更準(zhǔn)確。
?
參考:
- 易于理解虛假分享
- C ++今日博客,虛假分享再次點(diǎn)擊!
- Dobbs博士的文章:消除虛假分享
- 在嘗試消除Java中的錯(cuò)誤共享時(shí)要小心
- Bolosky,WJ和Scott,ML 1993.?虛假共享及其對(duì)共享內(nèi)存性能的影響
總結(jié)
以上是生活随笔為你收集整理的好理解的Java内存虚假共享(False Sharing)性能损耗以及解决方案的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 家庭用的自来水水管一般多少钱一米?
- 下一篇: 不要千言万语,一组漫画让你秒懂最终一致性