偽共享和緩存行
在計(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)證明我們的理論分析的正確性,參考下面的代碼段。
- package com.robert.concurrency.cacheline;
- /**
- *
- * @author:李艷鵬
- * @since:Jun 11, 2017 1:01:29 AM
- * @version: 1.0
- */
- public final class FalseSharingDemo {
- // 測(cè)試用的線程數(shù)
- private final static int NUM_THREADS = 4;
- // 測(cè)試的次數(shù)
- private final static int NUM_TEST_TIMES = 10;
- // 無(wú)填充、無(wú)緩存行對(duì)齊的對(duì)象類(lèi)
- static class PlainHotVariable {
- public volatile long value = 0L;
- }
- // 有填充、有緩存行對(duì)齊的對(duì)象類(lèi)
- 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 {
- // 開(kāi)啟多個(gè)線程進(jìn)行測(cè)試
- 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ì)每次測(cè)試使用的時(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)行無(wú)填充、無(wú)緩存行對(duì)齊的測(cè)試
- long t1 = runOneTest(plainHotVariables);
- AlignHotVariable[] alignHotVariable = new AlignHotVariable[theadNum];
- for (int i = 0; i < NUM_THREADS; i++) {
- alignHotVariable[i] = new AlignHotVariable();
- }
- // 進(jìn)行有填充、有緩存行對(duì)齊的測(cè)試
- 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ì)齊的測(cè)試場(chǎng)景下響應(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次測(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)。
- ...
- // 無(wú)填充、無(wú)緩存行對(duì)齊的對(duì)象類(lèi)
- 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修飾的變量,這樣其他的線程才能看到它的變化。
接下來(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)。
- ...
- // 有填充、有緩存行對(duì)齊的對(duì)象類(lèi)
- 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ù)組,我們開(kāi)啟4個(gè)線程,每個(gè)線程對(duì)數(shù)組中的其中一個(gè)變量進(jìn)行大量的存取操作,然后,對(duì)比測(cè)試結(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%
從上面的測(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è)由于緩存行影響性能的示例,代碼如下所示。
- 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è)長(zhǎng)整形
- private final static int CACHE_LINE_LONG_NUM = 8;
- // 用于測(cè)試的緩存行的數(shù)量
- private final static int LINE_NUM = 1024 * 1024;
- // 一次測(cè)試的次數(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)行順序讀取測(cè)試,期待在存取每個(gè)緩存行的第一個(gè)長(zhǎng)整形變量的時(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();
- // 按照緩存行的步長(zhǎng)進(jìn)行跳躍讀取測(cè)試,期待每次讀取一行中的一個(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ì)算順序訪問(wèn)數(shù)組的測(cè)試場(chǎng)景下,響應(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è)長(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é)果如下所示:
- 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é)果中分析得到,順序訪問(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)系】