自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

偽共享和緩存行

開(kāi)發(fā) 開(kāi)發(fā)工具
偽共享指的是在多個(gè)線程同時(shí)讀寫(xiě)同一個(gè)緩存行的不同變量的時(shí)候,盡管這些變量之間沒(méi)有任何關(guān)系,但是在多個(gè)線程之間仍然需要同步,從而導(dǎo)致性能下降的情況。

[[196784]]

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

為了通過(guò)增加線程數(shù)來(lái)達(dá)到計(jì)算能力的水平擴(kuò)展,我們必須確保多個(gè)線程不能同時(shí)對(duì)一個(gè)變量或者緩存行進(jìn)行讀寫(xiě)。我們可以通過(guò)代碼走查的方式,定位多個(gè)線程讀寫(xiě)一個(gè)變量的情況,但是,要想知道多個(gè)線程讀寫(xiě)同一個(gè)緩存行的情況,我們必須先了解系統(tǒng)內(nèi)存的組織形式,如下圖所示。

系統(tǒng)的緩存結(jié)構(gòu)

從上圖看到,線程1在CPU核心1上讀寫(xiě)變量X,同時(shí)線程2在CPU核心2上讀寫(xiě)變量Y,不幸的是變量X和變量Y在同一個(gè)緩存行上,每一個(gè)線程為了對(duì)緩存行進(jìn)行讀寫(xiě),都要競(jìng)爭(zhēng)并獲得緩存行的讀寫(xiě)權(quán)限,如果線程2在CPU核心2上獲得了對(duì)緩存行進(jìn)行讀寫(xiě)的權(quán)限,那么線程1必須刷新它的緩存后才能在核心1上獲得讀寫(xiě)權(quán)限,這導(dǎo)致這個(gè)緩存行在不同的線程間多次通過(guò)L3緩存來(lái)交換最新的拷貝數(shù)據(jù),這極大的影響了多核心CPU的性能。如果這些CPU核心在不同的插槽上,性能會(huì)變得更糟。

現(xiàn)在,我們學(xué)習(xí)JVM對(duì)象的內(nèi)存模型。所有的Java對(duì)象都有8字節(jié)的對(duì)象頭,前四個(gè)字節(jié)用來(lái)保存對(duì)象的哈希碼和鎖的狀態(tài),前3個(gè)字節(jié)用來(lái)存儲(chǔ)哈希碼,最后一個(gè)字節(jié)用來(lái)存儲(chǔ)鎖狀態(tài),一旦對(duì)象上鎖,這4個(gè)字節(jié)都會(huì)被拿出對(duì)象外,并用指針進(jìn)行鏈接。剩下4個(gè)字節(jié)用來(lái)存儲(chǔ)對(duì)象所屬類(lèi)的引用。對(duì)于數(shù)組來(lái)講,還有一個(gè)保存數(shù)組大小的變量,為4字節(jié)。每一個(gè)對(duì)象的大小都會(huì)對(duì)齊到8字節(jié)的倍數(shù),不夠8字節(jié)部分需要填充。為了保證效率,Java編譯器在編譯Java對(duì)象的時(shí)候,通過(guò)字段類(lèi)型對(duì)Java對(duì)象的字段進(jìn)行排序,如下表所示

因此,我們可以在任何字段之間通過(guò)填充長(zhǎng)整型的變量把熱點(diǎn)變量隔離在不同的緩存行中,通過(guò)減少偽同步,在多核心CPU中能夠極大的提高效率。

下面,我們通過(guò)一個(gè)測(cè)試用例來(lái)證明我們的理論分析的正確性,參考下面的代碼段。

  1. package com.robert.concurrency.cacheline; 
  2.  
  3. /** 
  4.  *  
  5.  * @author:李艷鵬 
  6.  * @since:Jun 11, 2017 1:01:29 AM 
  7.  * @version: 1.0 
  8.  */ 
  9. public final class FalseSharingDemo { 
  10.  
  11.     // 測(cè)試用的線程數(shù) 
  12.     private final static int NUM_THREADS = 4; 
  13.  
  14.     // 測(cè)試的次數(shù) 
  15.     private final static int NUM_TEST_TIMES = 10; 
  16.  
  17.     // 無(wú)填充、無(wú)緩存行對(duì)齊的對(duì)象類(lèi) 
  18.     static class PlainHotVariable { 
  19.  
  20.         public volatile long value = 0L; 
  21.     } 
  22.  
  23.     // 有填充、有緩存行對(duì)齊的對(duì)象類(lèi) 
  24.     static final class AlignHotVariable extends PlainHotVariable { 
  25.  
  26.         public long p1, p2, p3, p4, p5, p6; 
  27.     } 
  28.  
  29.     static final class CompetitorThread extends Thread { 
  30.  
  31.         private final static long ITERATIONS = 500L * 1000L * 1000L; 
  32.  
  33.         private PlainHotVariable plainHotVariable; 
  34.  
  35.         public CompetitorThread(final PlainHotVariable plainHotVariable) { 
  36.             this.plainHotVariable = plainHotVariable; 
  37.         } 
  38.  
  39.         @Override 
  40.         public void run() { 
  41.             // 一個(gè)線程對(duì)一個(gè)變量進(jìn)行大量的存取操作 
  42.             for (int i = 0; i < ITERATIONS; i++) { 
  43.                 plainHotVariable.value = i; 
  44.             } 
  45.  
  46.         } 
  47.  
  48.     } 
  49.  
  50.     public static long runOneTest(PlainHotVariable[] plainHotVariables) throws Exception { 
  51.         // 開(kāi)啟多個(gè)線程進(jìn)行測(cè)試 
  52.         CompetitorThread[] competitorThreads = new CompetitorThread[plainHotVariables.length]; 
  53.         for (int i = 0; i < plainHotVariables.length; i++) { 
  54.             competitorThreads[i] = new CompetitorThread(plainHotVariables[i]); 
  55.         } 
  56.  
  57.         final long start = System.nanoTime(); 
  58.         for (Thread t : competitorThreads) { 
  59.             t.start(); 
  60.         } 
  61.  
  62.         for (Thread t : competitorThreads) { 
  63.             t.join(); 
  64.         } 
  65.  
  66.         // 統(tǒng)計(jì)每次測(cè)試使用的時(shí)間 
  67.         return System.nanoTime() - start; 
  68.     } 
  69.  
  70.     public static boolean runOneCompare(int theadNum) throws Exception { 
  71.         PlainHotVariable[] plainHotVariables = new PlainHotVariable[theadNum]; 
  72.  
  73.         for (int i = 0; i < theadNum; i++) { 
  74.             plainHotVariables[i] = new PlainHotVariable(); 
  75.         } 
  76.  
  77.         // 進(jìn)行無(wú)填充、無(wú)緩存行對(duì)齊的測(cè)試 
  78.         long t1 = runOneTest(plainHotVariables); 
  79.  
  80.         AlignHotVariable[] alignHotVariable = new AlignHotVariable[theadNum]; 
  81.  
  82.         for (int i = 0; i < NUM_THREADS; i++) { 
  83.             alignHotVariable[i] = new AlignHotVariable(); 
  84.         } 
  85.  
  86.         // 進(jìn)行有填充、有緩存行對(duì)齊的測(cè)試 
  87.  
  88.         long t2 = runOneTest(alignHotVariable); 
  89.  
  90.         System.out.println("Plain: " + t1); 
  91.         System.out.println("Align: " + t2); 
  92.  
  93.         // 返回對(duì)比結(jié)果 
  94.         return t1 > t2; 
  95.     } 
  96.  
  97.     public static void runOneSuit(int threadsNum, int testNum) throws Exception { 
  98.         int expectedCount = 0; 
  99.         for (int i = 0; i < testNum; i++) { 
  100.             if (runOneCompare(threadsNum)) 
  101.                 expectedCount++; 
  102.         } 
  103.  
  104.         // 計(jì)算有填充、有緩存行對(duì)齊的測(cè)試場(chǎng)景下響應(yīng)時(shí)間更短的情況的概率 
  105.         System.out.println("Radio (Plain < Align) : " + expectedCount * 100D / testNum + "%"); 
  106.     } 
  107.  
  108.     public static void main(String[] args) throws Exception { 
  109.         runOneSuit(NUM_THREADS, NUM_TEST_TIMES); 
  110.     } 

在上面的代碼示例中,我們做了10次測(cè)試,每次對(duì)不填充的變量和填充的變量進(jìn)行大量讀寫(xiě)所花費(fèi)的時(shí)間對(duì)比來(lái)判斷偽同步對(duì)性能的影響。在每次對(duì)比中,我們首先創(chuàng)建了具有4個(gè)普通對(duì)象的數(shù)組,每個(gè)對(duì)象里包含一個(gè)長(zhǎng)整形的變量,由于長(zhǎng)整形占用8個(gè)字節(jié),對(duì)象頭占用8個(gè)字節(jié),每個(gè)對(duì)象占用16個(gè)字節(jié),4個(gè)對(duì)象占用64個(gè)字節(jié),因此,他們很有可能在同一個(gè)緩存行內(nèi)。

  1. ... 
  2.  
  3.     // 無(wú)填充、無(wú)緩存行對(duì)齊的對(duì)象類(lèi) 
  4.     static class PlainHotVariable { 
  5.  
  6.         public volatile long value = 0L; 
  7.     } 
  8.  
  9.     ... 
  10.  
  11.     PlainHotVariable[] plainHotVariables = new PlainHotVariable[theadNum]; 
  12.  
  13.     for (int i = 0; i < theadNum; i++) { 
  14.         plainHotVariables[i] = new PlainHotVariable(); 
  15.     } 
  16.     ... 

注意,這里value必須是volatile修飾的變量,這樣其他的線程才能看到它的變化。

接下來(lái),我們創(chuàng)建了具有4個(gè)填充對(duì)象的數(shù)組,每個(gè)對(duì)象里包含一個(gè)長(zhǎng)整形的變量,后面填充6個(gè)長(zhǎng)整形的變量,由于長(zhǎng)整形占用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ì)有效的消除偽競(jìng)爭(zhēng)。

  1. ... 
  2.  
  3.     // 有填充、有緩存行對(duì)齊的對(duì)象類(lèi) 
  4.     static final class AlignHotVariable extends PlainHotVariable { 
  5.  
  6.         public long p1, p2, p3, p4, p5, p6; 
  7.     } 
  8.  
  9.     ... 
  10.  
  11.     AlignHotVariable[] alignHotVariable = new AlignHotVariable[theadNum]; 
  12.  
  13.     for (int i = 0; i < NUM_THREADS; i++) { 
  14.         alignHotVariable[i] = new AlignHotVariable(); 
  15.     } 
  16.     ... 

對(duì)于上面創(chuàng)建的對(duì)象數(shù)組,我們開(kāi)啟4個(gè)線程,每個(gè)線程對(duì)數(shù)組中的其中一個(gè)變量進(jìn)行大量的存取操作,然后,對(duì)比測(cè)試結(jié)果如下。

1線程:

  1. Plain: 3880440094 
  2. Align: 3603752245 
  3. Plain: 3639901291 
  4. Align: 3633625092 
  5. Plain: 3623244143 
  6. Align: 3840919263 
  7. Plain: 3601311736 
  8. Align: 3695416688 
  9. Plain: 3837399466 
  10. Align: 3629233967 
  11. Plain: 3672411584 
  12. Align: 3622377013 
  13. Plain: 3678894140 
  14. Align: 3614962801 
  15. Plain: 3685449655 
  16. Align: 3578069018 
  17. Plain: 3650083667 
  18. Align: 4108272016 
  19. Plain: 3643323254 
  20. Align: 3840311867 
  21. Radio (Plain > Align) : 60.0% 

2線程

  1. Plain: 17403262079 
  2. Align: 3946343388 
  3. Plain: 3868304700 
  4. Align: 3650775431 
  5. Plain: 12111598939 
  6. Align: 4224862180 
  7. Plain: 4805070043 
  8. Align: 4130750299 
  9. Plain: 15889926613 
  10. Align: 3901238050 
  11. Plain: 12059354004 
  12. Align: 3771834390 
  13. Plain: 16744207113 
  14. Align: 4433367085 
  15. Plain: 4090413088 
  16. Align: 3834753740 
  17. Plain: 11791092554 
  18. Align: 3952127226 
  19. Plain: 12125857773 
  20. Align: 4140062817 
  21. Radio (Plain > Align) : 100.0% 

4線程:

  1. Plain: 12714212746 
  2. Align: 7462938088 
  3. Plain: 12865714317 
  4. Align: 6962498317 
  5. Plain: 18767257391 
  6. Align: 7632201194 
  7. Plain: 12730329600 
  8. Align: 6955849781 
  9. Plain: 12246997913 
  10. Align: 7457147789 
  11. Plain: 17341965313 
  12. Align: 7333927073 
  13. Plain: 19363865296 
  14. Align: 7323193058 
  15. Plain: 12201435415 
  16. Align: 7279922233 
  17. Plain: 12810166367 
  18. Align: 7613635297 
  19. Plain: 19235104612 
  20. Align: 7398148996 
  21. Radio (Plain > Align) : 100.0% 

從上面的測(cè)試結(jié)果中可以看到,使用填充的數(shù)組進(jìn)行測(cè)試,花費(fèi)的時(shí)間普遍小于使用不填充的數(shù)組進(jìn)行測(cè)試的情況,并且隨著線程數(shù)的增加,使用不填充的數(shù)組的場(chǎng)景性能隨之下降,可伸縮性也變得越來(lái)越弱,見(jiàn)下圖所示。

無(wú)填充和有填充對(duì)比圖標(biāo)

盡管我們并不精確的知道系統(tǒng)如何分配我們的對(duì)象,但是,我們的測(cè)試結(jié)果驗(yàn)證了我們的理論分析的正確性。

實(shí)際上,著名的無(wú)鎖隊(duì)列Disruptor通過(guò)解決偽競(jìng)爭(zhēng)的問(wèn)題來(lái)提高效率,它通過(guò)在RingBuffer的游標(biāo)和BatchEventProcessor的序列變量后填充變量,使之與64字節(jié)大小的緩存行對(duì)齊來(lái)解決偽競(jìng)爭(zhēng)的問(wèn)題。

上面我們看到緩存行的機(jī)制在多線程環(huán)境下會(huì)產(chǎn)生偽同步,現(xiàn)在,我們學(xué)習(xí)另外一個(gè)由于緩存行影響性能的示例,代碼如下所示。

  1. package com.robert.concurrency.cacheline; 
  2.  
  3. /** 
  4.  *  
  5.  * @author:李艷鵬 
  6.  * @since:Jun 11, 2017 1:01:29 AM 
  7.  * @version: 1.0 
  8.  */ 
  9.  
  10. public final class CacheLineDemo { 
  11.  
  12.     // 緩存行的大小為64個(gè)字節(jié),即為8個(gè)長(zhǎng)整形 
  13.     private final static int CACHE_LINE_LONG_NUM = 8; 
  14.  
  15.     // 用于測(cè)試的緩存行的數(shù)量 
  16.     private final static int LINE_NUM = 1024 * 1024; 
  17.  
  18.     // 一次測(cè)試的次數(shù) 
  19.     private final static int NUM_TEST_TIMES = 10; 
  20.  
  21.     // 構(gòu)造能夠填充LINE_NUM個(gè)緩存行的數(shù)組 
  22.     private static final long[] values = new long[CACHE_LINE_LONG_NUM * LINE_NUM]; 
  23.  
  24.     public static long runOneTestWithAlign() { 
  25.  
  26.         final long start = System.nanoTime(); 
  27.  
  28.         // 進(jìn)行順序讀取測(cè)試,期待在存取每個(gè)緩存行的第一個(gè)長(zhǎng)整形變量的時(shí)候系統(tǒng)自動(dòng)緩存整個(gè)緩存行,本行的后續(xù)存取都會(huì)命中緩存 
  29.         for (int i = 0; i < CACHE_LINE_LONG_NUM * LINE_NUM; i++) 
  30.             values[i] = i; 
  31.  
  32.         return System.nanoTime() - start; 
  33.  
  34.     } 
  35.  
  36.     public static long runOneTestWithoutAlign() { 
  37.         final long start = System.nanoTime(); 
  38.  
  39.         // 按照緩存行的步長(zhǎng)進(jìn)行跳躍讀取測(cè)試,期待每次讀取一行中的一個(gè)元素,每次讀取都不會(huì)命中緩存 
  40.         for (int i = 0; i < CACHE_LINE_LONG_NUM; i++) 
  41.             for (int j = 0; j < LINE_NUM; j++) 
  42.                 values[j * CACHE_LINE_LONG_NUM + i] = i * j; 
  43.  
  44.         return System.nanoTime() - start; 
  45.     } 
  46.  
  47.     public static boolean runOneCompare() { 
  48.         long t1 = runOneTestWithAlign(); 
  49.         long t2 = runOneTestWithoutAlign(); 
  50.  
  51.         System.out.println("Sequential: " + t1); 
  52.         System.out.println("      Leap: " + t2); 
  53.  
  54.         return t1 < t2; 
  55.     } 
  56.  
  57.     public static void runOneSuit(int testNum) throws Exception { 
  58.         int expectedCount = 0; 
  59.         for (int i = 0; i < testNum; i++) { 
  60.             if (runOneCompare()) 
  61.                 expectedCount++; 
  62.         } 
  63.  
  64.         // 計(jì)算順序訪問(wèn)數(shù)組的測(cè)試場(chǎng)景下,響應(yīng)時(shí)間更短的情況的概率 
  65.  
  66.         System.out.println("Radio (Sequential < Leap): " + expectedCount * 100D / testNum + "%"); 
  67.     } 
  68.  
  69.     public static void main(String[] args) throws Exception { 
  70.         runOneSuit(NUM_TEST_TIMES); 
  71.     } 

在上面的示例中,我們創(chuàng)建了1024 1024 8個(gè)長(zhǎng)整形數(shù)組,首先,我們順序訪問(wèn)每一個(gè)長(zhǎng)整形,按照前面我們對(duì)緩存行的分析,每8個(gè)長(zhǎng)整形占用一個(gè)緩存行,那么也就是我們存取8個(gè)長(zhǎng)整形才需要去L3緩存交換一次數(shù)據(jù),大大的提高了緩存的使用效率。然后,我們換了一種方式進(jìn)行測(cè)試,每次跳躍性的訪問(wèn)數(shù)組,一次以一行為步長(zhǎng)跳躍,我們期待每次訪問(wèn)一個(gè)元素操作系統(tǒng)需要從L3緩存取數(shù)據(jù),結(jié)果如下所示:

  1. Sequential: 11092440 
  2.       Leap: 66234827 
  3. Sequential: 9961470 
  4.       Leap: 62903705 
  5. Sequential: 7785285 
  6.       Leap: 64447613 
  7. Sequential: 7981995 
  8.       Leap: 73487063 
  9. Sequential: 8779595 
  10.       Leap: 74127379 
  11. Sequential: 10012716 
  12.       Leap: 67089382 
  13. Sequential: 8437842 
  14.       Leap: 79442009 
  15. Sequential: 13377366 
  16.       Leap: 80074056 
  17. Sequential: 11428147 
  18.       Leap: 81245364 
  19. Sequential: 9514993 
  20.       Leap: 69569712 
  21. Radio (Sequential < Leap): 100.0% 

我們從上面的結(jié)果中分析得到,順序訪問(wèn)的速度每次都高于跳躍訪問(wèn)的速度,驗(yàn)證了我們前面對(duì)緩存行的理論分析。

這里我們看到,我們需要對(duì)JVM的實(shí)現(xiàn)機(jī)制以及操作系統(tǒng)內(nèi)核有所了解,才能找到系統(tǒng)性能的瓶頸,最終提高系統(tǒng)的性能,進(jìn)一步提高系統(tǒng)的用戶(hù)友好性。

點(diǎn)擊《偽共享和緩存行》閱讀原文。

【本文為51CTO專(zhuān)欄作者“李艷鵬”的原創(chuàng)稿件,轉(zhuǎn)載可通過(guò)作者簡(jiǎn)書(shū)號(hào)(李艷鵬)或51CTO專(zhuān)欄獲取聯(lián)系】

戳這里,看該作者更多好文

責(zé)任編輯:武曉燕 來(lái)源: 51CTO專(zhuān)欄
相關(guān)推薦

2023-12-26 10:08:57

緩存偽共享修飾結(jié)構(gòu)體

2019-12-17 14:24:11

CPU緩存偽共享

2012-12-17 14:54:55

算法緩存Java

2019-11-05 14:24:31

緩存雪崩框架

2021-08-05 16:10:03

進(jìn)程緩存緩存服務(wù)Java

2021-11-30 10:58:52

算法緩存技術(shù)

2022-12-12 08:39:09

CPUCache偽共享

2021-12-25 22:28:27

緩存穿透緩存擊穿緩存雪崩

2021-11-18 08:55:49

共享CPU內(nèi)存

2022-01-17 14:24:09

共享字節(jié)面試

2011-08-05 15:51:44

MySQL數(shù)據(jù)庫(kù)緩存

2017-08-23 13:21:31

2021-01-20 05:33:03

緩存ReadWriteLo高并發(fā)

2025-03-03 07:00:00

2023-08-30 10:28:02

LRU鏈表區(qū)域

2023-08-31 13:36:00

系統(tǒng)預(yù)讀失效

2013-06-14 10:12:22

共享并行

2021-03-01 11:53:15

面試偽共享CPU

2022-02-02 21:50:25

底層偽共享CPU

2024-11-05 13:50:12

點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)