新聞中心
在計(jì)算機(jī)系統(tǒng)中,內(nèi)存是以緩存行為單位存儲(chǔ)的,一個(gè)緩存行存儲(chǔ)字節(jié)的數(shù)量為2的倍數(shù),在不同的機(jī)器上,緩存行大小為32字節(jié)到256字節(jié)不等,通常來說為64字節(jié)。偽共享指的是在多個(gè)線程同時(shí)讀寫同一個(gè)緩存行的不同變量的時(shí)候,盡管這些變量之間沒有任何關(guān)系,但是在多個(gè)線程之間仍然需要同步,從而導(dǎo)致性能下降的情況。在對(duì)稱多處理器結(jié)構(gòu)的系統(tǒng)中,偽共享是影響性能的主要因素之一,由于很難通過走查代碼的方式定位偽共享的問題,因此,大家把偽共享稱為“性能殺手”。

公司主營業(yè)務(wù):成都網(wǎng)站建設(shè)、網(wǎng)站制作、移動(dòng)網(wǎng)站開發(fā)等業(yè)務(wù)。幫助企業(yè)客戶真正實(shí)現(xiàn)互聯(lián)網(wǎng)宣傳,提高企業(yè)的競爭能力。成都創(chuàng)新互聯(lián)公司是一支青春激揚(yáng)、勤奮敬業(yè)、活力青春激揚(yáng)、勤奮敬業(yè)、活力澎湃、和諧高效的團(tuán)隊(duì)。公司秉承以“開放、自由、嚴(yán)謹(jǐn)、自律”為核心的企業(yè)文化,感謝他們對(duì)我們的高要求,感謝他們從不同領(lǐng)域給我們帶來的挑戰(zhàn),讓我們激情的團(tuán)隊(duì)有機(jī)會(huì)用頭腦與智慧不斷的給客戶帶來驚喜。成都創(chuàng)新互聯(lián)公司推出大祥免費(fèi)做網(wǎng)站回饋大家。
為了通過增加線程數(shù)來達(dá)到計(jì)算能力的水平擴(kuò)展,我們必須確保多個(gè)線程不能同時(shí)對(duì)一個(gè)變量或者緩存行進(jìn)行讀寫。我們可以通過代碼走查的方式,定位多個(gè)線程讀寫一個(gè)變量的情況,但是,要想知道多個(gè)線程讀寫同一個(gè)緩存行的情況,我們必須先了解系統(tǒng)內(nèi)存的組織形式,如下圖所示。
系統(tǒng)的緩存結(jié)構(gòu)
從上圖看到,線程1在CPU核心1上讀寫變量X,同時(shí)線程2在CPU核心2上讀寫變量Y,不幸的是變量X和變量Y在同一個(gè)緩存行上,每一個(gè)線程為了對(duì)緩存行進(jìn)行讀寫,都要競爭并獲得緩存行的讀寫權(quán)限,如果線程2在CPU核心2上獲得了對(duì)緩存行進(jìn)行讀寫的權(quán)限,那么線程1必須刷新它的緩存后才能在核心1上獲得讀寫權(quán)限,這導(dǎo)致這個(gè)緩存行在不同的線程間多次通過L3緩存來交換最新的拷貝數(shù)據(jù),這極大的影響了多核心CPU的性能。如果這些CPU核心在不同的插槽上,性能會(huì)變得更糟。
現(xiàn)在,我們學(xué)習(xí)JVM對(duì)象的內(nèi)存模型。所有的Java對(duì)象都有8字節(jié)的對(duì)象頭,前四個(gè)字節(jié)用來保存對(duì)象的哈希碼和鎖的狀態(tài),前3個(gè)字節(jié)用來存儲(chǔ)哈希碼,最后一個(gè)字節(jié)用來存儲(chǔ)鎖狀態(tài),一旦對(duì)象上鎖,這4個(gè)字節(jié)都會(huì)被拿出對(duì)象外,并用指針進(jìn)行鏈接。剩下4個(gè)字節(jié)用來存儲(chǔ)對(duì)象所屬類的引用。對(duì)于數(shù)組來講,還有一個(gè)保存數(shù)組大小的變量,為4字節(jié)。每一個(gè)對(duì)象的大小都會(huì)對(duì)齊到8字節(jié)的倍數(shù),不夠8字節(jié)部分需要填充。為了保證效率,Java編譯器在編譯Java對(duì)象的時(shí)候,通過字段類型對(duì)Java對(duì)象的字段進(jìn)行排序,如下表所示
因此,我們可以在任何字段之間通過填充長整型的變量把熱點(diǎn)變量隔離在不同的緩存行中,通過減少偽同步,在多核心CPU中能夠極大的提高效率。
下面,我們通過一個(gè)測試用例來證明我們的理論分析的正確性,參考下面的代碼段。
- package com.robert.concurrency.cacheline;
- /**
- *
- * @author:李艷鵬
- * @since:Jun 11, 2017 1:01:29 AM
- * @version: 1.0
- */
- public final class FalseSharingDemo {
- // 測試用的線程數(shù)
- private final static int NUM_THREADS = 4;
- // 測試的次數(shù)
- private final static int NUM_TEST_TIMES = 10;
- // 無填充、無緩存行對(duì)齊的對(duì)象類
- static class PlainHotVariable {
- public volatile long value = 0L;
- }
- // 有填充、有緩存行對(duì)齊的對(duì)象類
- static final class AlignHotVariable extends PlainHotVariable {
- public long p1, p2, p3, p4, p5, p6;
- }
- static final class CompetitorThread extends Thread {
- private final static long ITERATIONS = 500L * 1000L * 1000L;
- private PlainHotVariable plainHotVariable;
- public CompetitorThread(final PlainHotVariable plainHotVariable) {
- this.plainHotVariable = plainHotVariable;
- }
- @Override
- public void run() {
- // 一個(gè)線程對(duì)一個(gè)變量進(jìn)行大量的存取操作
- for (int i = 0; i < ITERATIONS; i++) {
- plainHotVariable.value = i;
- }
- }
- }
- public static long runOneTest(PlainHotVariable[] plainHotVariables) throws Exception {
- // 開啟多個(gè)線程進(jìn)行測試
- CompetitorThread[] competitorThreads = new CompetitorThread[plainHotVariables.length];
- for (int i = 0; i < plainHotVariables.length; i++) {
- competitorThreads[i] = new CompetitorThread(plainHotVariables[i]);
- }
- final long start = System.nanoTime();
- for (Thread t : competitorThreads) {
- t.start();
- }
- for (Thread t : competitorThreads) {
- t.join();
- }
- // 統(tǒng)計(jì)每次測試使用的時(shí)間
- return System.nanoTime() - start;
- }
- public static boolean runOneCompare(int theadNum) throws Exception {
- PlainHotVariable[] plainHotVariables = new PlainHotVariable[theadNum];
- for (int i = 0; i < theadNum; i++) {
- plainHotVariables[i] = new PlainHotVariable();
- }
- // 進(jìn)行無填充、無緩存行對(duì)齊的測試
- long t1 = runOneTest(plainHotVariables);
- AlignHotVariable[] alignHotVariable = new AlignHotVariable[theadNum];
- for (int i = 0; i < NUM_THREADS; i++) {
- alignHotVariable[i] = new AlignHotVariable();
- }
- // 進(jìn)行有填充、有緩存行對(duì)齊的測試
- long t2 = runOneTest(alignHotVariable);
- System.out.println("Plain: " + t1);
- System.out.println("Align: " + t2);
- // 返回對(duì)比結(jié)果
- return t1 > t2;
- }
- public static void runOneSuit(int threadsNum, int testNum) throws Exception {
- int expectedCount = 0;
- for (int i = 0; i < testNum; i++) {
- if (runOneCompare(threadsNum))
- expectedCount++;
- }
- // 計(jì)算有填充、有緩存行對(duì)齊的測試場景下響應(yīng)時(shí)間更短的情況的概率
- System.out.println("Radio (Plain < Align) : " + expectedCount * 100D / testNum + "%");
- }
- public static void main(String[] args) throws Exception {
- runOneSuit(NUM_THREADS, NUM_TEST_TIMES);
- }
- }
在上面的代碼示例中,我們做了10次測試,每次對(duì)不填充的變量和填充的變量進(jìn)行大量讀寫所花費(fèi)的時(shí)間對(duì)比來判斷偽同步對(duì)性能的影響。在每次對(duì)比中,我們首先創(chuàng)建了具有4個(gè)普通對(duì)象的數(shù)組,每個(gè)對(duì)象里包含一個(gè)長整形的變量,由于長整形占用8個(gè)字節(jié),對(duì)象頭占用8個(gè)字節(jié),每個(gè)對(duì)象占用16個(gè)字節(jié),4個(gè)對(duì)象占用64個(gè)字節(jié),因此,他們很有可能在同一個(gè)緩存行內(nèi)。
- ...
- // 無填充、無緩存行對(duì)齊的對(duì)象類
- static class PlainHotVariable {
- public volatile long value = 0L;
- }
- ...
- PlainHotVariable[] plainHotVariables = new PlainHotVariable[theadNum];
- for (int i = 0; i < theadNum; i++) {
- plainHotVariables[i] = new PlainHotVariable();
- }
- ...
注意,這里value必須是volatile修飾的變量,這樣其他的線程才能看到它的變化。
接下來,我們創(chuàng)建了具有4個(gè)填充對(duì)象的數(shù)組,每個(gè)對(duì)象里包含一個(gè)長整形的變量,后面填充6個(gè)長整形的變量,由于長整形占用8個(gè)字節(jié),對(duì)象頭占用8個(gè)字節(jié),每個(gè)對(duì)象占用64個(gè)字節(jié),4個(gè)對(duì)象占用4個(gè)64字節(jié)大小的空間,因此,他們每個(gè)對(duì)象正好與64字節(jié)對(duì)齊,會(huì)有效的消除偽競爭。
- ...
- // 有填充、有緩存行對(duì)齊的對(duì)象類
- static final class AlignHotVariable extends PlainHotVariable {
- public long p1, p2, p3, p4, p5, p6;
- }
- ...
- AlignHotVariable[] alignHotVariable = new AlignHotVariable[theadNum];
- for (int i = 0; i < NUM_THREADS; i++) {
- alignHotVariable[i] = new AlignHotVariable();
- }
- ...
對(duì)于上面創(chuàng)建的對(duì)象數(shù)組,我們開啟4個(gè)線程,每個(gè)線程對(duì)數(shù)組中的其中一個(gè)變量進(jìn)行大量的存取操作,然后,對(duì)比測試結(jié)果如下。
1線程:
- Plain: 3880440094
- Align: 3603752245
- Plain: 3639901291
- Align: 3633625092
- Plain: 3623244143
- Align: 3840919263
- Plain: 3601311736
- Align: 3695416688
- Plain: 3837399466
- Align: 3629233967
- Plain: 3672411584
- Align: 3622377013
- Plain: 3678894140
- Align: 3614962801
- Plain: 3685449655
- Align: 3578069018
- Plain: 3650083667
- Align: 4108272016
- Plain: 3643323254
- Align: 3840311867
- Radio (Plain > Align) : 60.0%
2線程
- Plain: 17403262079
- Align: 3946343388
- Plain: 3868304700
- Align: 3650775431
- Plain: 12111598939
- Align: 4224862180
- Plain: 4805070043
- Align: 4130750299
- Plain: 15889926613
- Align: 3901238050
- Plain: 12059354004
- Align: 3771834390
- Plain: 16744207113
- Align: 4433367085
- Plain: 4090413088
- Align: 3834753740
- Plain: 11791092554
- Align: 3952127226
- Plain: 12125857773
- Align: 4140062817
- Radio (Plain > Align) : 100.0%
4線程:
- Plain: 12714212746
- Align: 7462938088
- Plain: 12865714317
- Align: 6962498317
- Plain: 18767257391
- Align: 7632201194
- Plain: 12730329600
- Align: 6955849781
- Plain: 12246997913
- Align: 7457147789
- Plain: 17341965313
- Align: 7333927073
- Plain: 19363865296
- Align: 7323193058
- Plain: 12201435415
- Align: 7279922233
- Plain: 12810166367
- Align: 7613635297
- Plain: 19235104612
- Align: 7398148996
- Radio (Plain > Align) : 100.0%
從上面的測試結(jié)果中可以看到,使用填充的數(shù)組進(jìn)行測試,花費(fèi)的時(shí)間普遍小于使用不填充的數(shù)組進(jìn)行測試的情況,并且隨著線程數(shù)的增加,使用不填充的數(shù)組的場景性能隨之下降,可伸縮性也變得越來越弱,見下圖所示。
無填充和有填充對(duì)比圖標(biāo)
盡管我們并不精確的知道系統(tǒng)如何分配我們的對(duì)象,但是,我們的測試結(jié)果驗(yàn)證了我們的理論分析的正確性。
實(shí)際上,著名的無鎖隊(duì)列Disruptor通過解決偽競爭的問題來提高效率,它通過在RingBuffer的游標(biāo)和BatchEventProcessor的序列變量后填充變量,使之與64字節(jié)大小的緩存行對(duì)齊來解決偽競爭的問題。
上面我們看到緩存行的機(jī)制在多線程環(huán)境下會(huì)產(chǎn)生偽同步,現(xiàn)在,我們學(xué)習(xí)另外一個(gè)由于緩存行影響性能的示例,代碼如下所示。
- package com.robert.concurrency.cacheline;
- /**
- *
- * @author:李艷鵬
- * @since:Jun 11, 2017 1:01:29 AM
- * @version: 1.0
- */
- public final class CacheLineDemo {
- // 緩存行的大小為64個(gè)字節(jié),即為8個(gè)長整形
- private final static int CACHE_LINE_LONG_NUM = 8;
- // 用于測試的緩存行的數(shù)量
- private final static int LINE_NUM = 1024 * 1024;
- // 一次測試的次數(shù)
- private final static int NUM_TEST_TIMES = 10;
- // 構(gòu)造能夠填充LINE_NUM個(gè)緩存行的數(shù)組
- private static final long[] values = new long[CACHE_LINE_LONG_NUM * LINE_NUM];
- public static long runOneTestWithAlign() {
- final long start = System.nanoTime();
- // 進(jìn)行順序讀取測試,期待在存取每個(gè)緩存行的第一個(gè)長整形變量的時(shí)候系統(tǒng)自動(dòng)緩存整個(gè)緩存行,本行的后續(xù)存取都會(huì)命中緩存
- for (int i = 0; i < CACHE_LINE_LONG_NUM * LINE_NUM; i++)
- values[i] = i;
- return System.nanoTime() - start;
- }
- public static long runOneTestWithoutAlign() {
- final long start = System.nanoTime();
- // 按照緩存行的步長進(jìn)行跳躍讀取測試,期待每次讀取一行中的一個(gè)元素,每次讀取都不會(huì)命中緩存
- for (int i = 0; i < CACHE_LINE_LONG_NUM; i++)
- for (int j = 0; j < LINE_NUM; j++)
- values[j * CACHE_LINE_LONG_NUM + i] = i * j;
- return System.nanoTime() - start;
- }
- public static boolean runOneCompare() {
- long t1 = runOneTestWithAlign();
- long t2 = runOneTestWithoutAlign();
- System.out.println("Sequential: " + t1);
- System.out.println(" Leap: " + t2);
- return t1 < t2;
- }
- public static void runOneSuit(int testNum) throws Exception {
- int expectedCount = 0;
- for (int i = 0; i < testNum; i++) {
- if (runOneCompare())
- expectedCount++;
- }
- // 計(jì)算順序訪問數(shù)組的測試場景下,響應(yīng)時(shí)間更短的情況的概率
- System.out.println("Radio (Sequential < Leap): " + expectedCount * 100D / testNum + "%");
- }
- public static void main(String[] args) throws Exception {
- runOneSuit(NUM_TEST_TIMES);
- }
- }
在上面的示例中,我們創(chuàng)建了1024 1024 8個(gè)長整形數(shù)組,首先,我們順序訪問每一個(gè)長整形,按照前面我們對(duì)緩存行的分析,每8個(gè)長整形占用一個(gè)緩存行,那么也就是我們存取8個(gè)長整形才需要去L3緩存交換一次數(shù)據(jù),大大的提高了緩存的使用效率。然后,我們換了一種方式進(jìn)行測試,每次跳躍性的訪問數(shù)組,一次以一行為步長跳躍,我們期待每次訪問一個(gè)元素操作系統(tǒng)需要從L3緩存取數(shù)據(jù),結(jié)果如下所示:
- Sequential: 11092440
- Leap: 66234827
- Sequential: 9961470
- Leap: 62903705
- Sequential: 7785285
- Leap: 64447613
- Sequential: 7981995
- Leap: 73487063
- Sequential: 8779595
- Leap: 74127379
- Sequential: 10012716
- Leap: 67089382
- Sequential: 8437842
- Leap: 79442009
- Sequential: 13377366
- Leap: 80074056
- Sequential: 11428147
- Leap: 81245364
- Sequential: 9514993
- Leap: 69569712
- Radio (Sequential < Leap): 100.0%
我們從上面的結(jié)果中分析得到,順序訪問的速度每次都高于跳躍訪問的速度,驗(yàn)證了我們前面對(duì)緩存行的理論分析。
這里我們看到,我們需要對(duì)JVM的實(shí)現(xiàn)機(jī)制以及操作系統(tǒng)內(nèi)核有所了解,才能找到系統(tǒng)性能的瓶頸,最終提高系統(tǒng)的性能,進(jìn)一步提高系統(tǒng)的用戶友好性。
點(diǎn)擊《偽共享和緩存行》閱讀原文。
【本文為專欄作者“李艷鵬”的原創(chuàng)稿件,轉(zhuǎn)載可通過作者簡書號(hào)(李艷鵬)或?qū)讷@取聯(lián)系】
戳這里,看該作者更多好文
網(wǎng)站名稱:偽共享和緩存行
網(wǎng)站地址:http://fisionsoft.com.cn/article/dpcgjeg.html


咨詢
建站咨詢
