cpu緩存?zhèn)喂蚕韱?wèn)題

一、問(wèn)題引入

由一個(gè)案例引進(jìn)拉讯,先上代碼

public class NoCacheLinePadding{

    private volatile long cacheLine1 = 1L;

    private volatile long cacheLine2 = 1L;

    public void setCacheLine1(long cacheLine1) {
        this.cacheLine1 = cacheLine1;
    }

    public void setCacheLine2(long cacheLine2) {
        this.cacheLine2 = cacheLine2;
    }

    public long getCacheLine1() {
        return cacheLine1;
    }

    public long getCacheLine2() {
        return cacheLine2;
    }
}


    void testNoCacheLine() throws Exception{
        CountDownLatch countDownLatch = new CountDownLatch(2);
        NoCacheLinePadding noCacheLinePadding = new NoCacheLinePadding();
        Thread a = new Thread(() -> {
            for (int i=0;i<100000000;i++) {
                noCacheLinePadding.setCacheLine1(i);
            }
            countDownLatch.countDown();
        });

        Thread b = new Thread(() -> {
            for (int i=0;i<100000000;i++) {
                noCacheLinePadding.setCacheLine2(i);
            }
            countDownLatch.countDown();

        });

        long start = System.nanoTime();
        a.start();
        b.start();
        countDownLatch.await();

        long end = System.nanoTime();
        System.out.println("cache1= "+noCacheLinePadding.getCacheLine1()+",cache2="+noCacheLinePadding.getCacheLine2()+",耗時(shí): "+ (end - start)/ 1000000 +"毫秒" );

    }

上面是一個(gè) 由兩個(gè)線程分別去循環(huán)1億次去修改一個(gè)對(duì)象中兩個(gè)不同屬性的測(cè)試用例。
測(cè)試結(jié)果:cache1= 99999999,cache2=99999999,耗時(shí): 461毫秒

/**
 * @author zhangxiaohu
 * @descript p0,p1,p2,p3,p4,p5,p6 p9,p10,p11,p12,p13,p14,p15 是緩存行填充,解決cpu緩存?zhèn)喂蚕韱?wèn)題
 * @date 2023-4-11
 */
public class CacheLinePadding{
    private volatile long p0,p1,p2,p3,p4,p5,p6;
    private volatile long cacheLine1 = 1L;
    private volatile long p9,p10,p11,p12,p13,p14,p15;
    private volatile long cacheLine2 = 1L;

    public void setCacheLine1(long cacheLine1) {
        this.cacheLine1 = cacheLine1;
    }

    public void setCacheLine2(long cacheLine2) {
        this.cacheLine2 = cacheLine2;
    }

    public long getCacheLine1() {
        return cacheLine1;
    }

    public long getCacheLine2() {
        return cacheLine2;
    }
}


    void testCacheLine() throws Exception{
        CountDownLatch countDownLatch = new CountDownLatch(2);
        CacheLinePadding cacheLinePadding = new CacheLinePadding();
        Thread a = new Thread(() -> {
            for (int i=0;i<100000000;i++) {
                cacheLinePadding.setCacheLine1(i);
            }
            countDownLatch.countDown();
        });

        Thread b = new Thread(() -> {
            for (int i=0;i<100000000;i++) {
                cacheLinePadding.setCacheLine2(i);
            }
            countDownLatch.countDown();

        });

        long start = System.nanoTime();
        a.start();
        b.start();
        countDownLatch.await();

        long end = System.nanoTime();
        System.out.println("cache1= "+cacheLinePadding.getCacheLine1()+",cache2="+cacheLinePadding.getCacheLine2()+",耗時(shí): "+ (end - start)/ 1000000 +"毫秒" );

    }

這個(gè)測(cè)試用例和第一個(gè)基本一致,不同的是我們?cè)趯?duì)象中加了一些看似多余的屬性p0,p1,p2,p3,p4,p5,p6 p9,p10,p11,p12,p13,p14,p15(下文會(huì)解釋為什么要這么做)
測(cè)試結(jié)果:cache1= 99999999,cache2=99999999,耗時(shí): 109毫秒

問(wèn)題:第二個(gè)測(cè)試用例只是多加入了一些多余的屬性降淮,為什么時(shí)間消耗 和 第一個(gè)測(cè)試用例的時(shí)間消耗 差距將近 5 倍?

為了搞清楚cpu緩存?zhèn)喂蚕淼母拍畈龋旅嫖覀儗囊韵聨讉€(gè)方面來(lái)解釋佳鳖。

  • cpu緩存結(jié)構(gòu)
  • cpu緩存?zhèn)喂蚕硎窃趺串a(chǎn)生的
  • 如何解決cpu緩存?zhèn)喂蚕淼膯?wèn)題

2.1、cpu緩存結(jié)構(gòu)

隨著計(jì)算機(jī)的發(fā)展媒惕,CPU 和內(nèi)存的訪問(wèn)性能相差越來(lái)越?系吩,于是就在 CPU 內(nèi)部嵌?了 CPUCache(?速緩存),CPU Cache 離 CPU 核?相當(dāng)近妒蔚,因此它的訪問(wèn)速度是很快的穿挨,于是它充當(dāng)了 CPU 與內(nèi)存之間的緩存??月弛。CPU Cache 通常分為三級(jí)緩存:L1 Cache、L2 Cache科盛、L3 Cache帽衙,級(jí)別越低的離 CPU 核?越近,訪問(wèn)速度也快贞绵,但是存儲(chǔ)容量相對(duì)就會(huì)越?厉萝。其中,在多核?的 CPU ?但壮,每個(gè)核?
都有各?的 L1/L2 Cache冀泻,? L3 Cache 是所有核?共享使?的。從上圖也可以看到蜡饵,從上往下弹渔,存儲(chǔ)設(shè)備的容量會(huì)越?,?訪問(wèn)速度會(huì)越慢溯祸≈ǎ可以看到, CPU 訪問(wèn) L1 Cache 速度?訪問(wèn)內(nèi)存快 100 倍焦辅,這就是為什么 CPU ?會(huì)有L1~L3 Cache 的原因博杖,?的就是把 Cache 作為 CPU 與內(nèi)存之間的緩存層,以減少對(duì)內(nèi)存的訪問(wèn)頻率筷登。


image.png

CPU 從內(nèi)存中讀取數(shù)據(jù)到 Cache 的時(shí)候剃根,并不是?個(gè)字節(jié)?個(gè)字節(jié)讀取,?是?塊?塊的?式來(lái)讀取數(shù)據(jù)的前方,這?塊?塊的數(shù)據(jù)被稱為 CPU Line(緩存?)狈醉,所以 CPU Line 是 CPU從內(nèi)存讀取數(shù)據(jù)到 Cache 的單位?于 CPU Line ??,在 Linux 系統(tǒng)可以?下?的?式查看到惠险,大部分服務(wù)器的Cache Line ??是 64 字節(jié)苗傅,也就意味著 L1 Cache ?次載?數(shù)據(jù)的??是 64 字節(jié)

那么對(duì)數(shù)組的加載, CPU 就會(huì)加載數(shù)組??連續(xù)的多個(gè)數(shù)據(jù)到 Cache ?班巩,因此我們應(yīng)該按照物理內(nèi)存地址分布的順序去訪問(wèn)元素渣慕,這樣訪問(wèn)數(shù)組元素的時(shí)候,Cache 命中率就會(huì)很?抱慌,于是就能減少?gòu)膬?nèi)存讀取數(shù)據(jù)的頻率逊桦, 從?可提?程序的性能,但是在我們不使?數(shù)組抑进,?是使?單獨(dú)的變量的時(shí)候卫袒,則會(huì)有 Cache 偽共享的問(wèn)題,Cache 偽共享問(wèn)題上是?個(gè)性能殺?单匣,我們應(yīng)該要規(guī)避它夕凝。接下來(lái)宝穗,就來(lái)看看 Cache 偽共享是什么??如何避免這個(gè)問(wèn)題码秉。

2.2涩搓、cpu緩存?zhèn)喂蚕硎侨绾萎a(chǎn)生的

還是以我們文章開頭的代碼舉例現(xiàn)在假設(shè)有?個(gè)雙核?的 CPU厘肮,這兩個(gè) CPU 核?并?運(yùn)?著兩個(gè)不同的線程电爹,它們同時(shí)從內(nèi)存中讀取兩個(gè)不同的數(shù)據(jù)诞外,分別是類型為 long 的變量 A(代碼中的cacheline1) 和 B(代碼中的cacheline2),這個(gè)兩個(gè)數(shù)據(jù)的地址在物理內(nèi)存上是連續(xù)的府蔗,如果 Cahce Line 的??是 64 字節(jié)晋控,并且變量 A 在 Cahce Line 的開頭位置,那么這兩個(gè)數(shù)據(jù)是位于同?個(gè) Cache Line 中姓赤,?因?yàn)?CPU Line 是 CPU 從內(nèi)存讀取數(shù)據(jù)到 Cache 的單位赡译,所以這兩個(gè)數(shù)據(jù)會(huì)被同時(shí)讀?到了兩個(gè) CPU 核?中各? Cache 中

image.png

我們來(lái)思考?個(gè)問(wèn)題,如果這兩個(gè)不同核?的線程分別修改不同的數(shù)據(jù)不铆,?如 1 號(hào) CPU 核?的線程只修改了 變量 A蝌焚,或 2 號(hào) CPU 核?的線程的線程只修改了變量 B,會(huì)發(fā)?什么呢

①. 最開始變量 A 和 B 都還不在 Cache ??誓斥,假設(shè) 1 號(hào)核?綁定了線程 A只洒,2 號(hào)核?綁定了線程 B,線程 A 只會(huì)讀寫變量 A劳坑,線程 B 只會(huì)讀寫變量 B


image.png

②. 1 號(hào)核?讀取變量 A毕谴,由于 CPU 從內(nèi)存讀取數(shù)據(jù)到 Cache 的單位是 Cache Line,也正好變量 A 和 變量 B 的數(shù)據(jù)歸屬于同?個(gè) Cache Line距芬,所以 A 和 B 的數(shù)據(jù)都會(huì)被加載到Cache涝开,并將此 Cache Line 標(biāo)記為「獨(dú)占」?fàn)顟B(tài)


image.png

③. 接著,2 號(hào)核?開始從內(nèi)存?讀取變量 B蔑穴,同樣的也是讀取 Cache Line ??的數(shù)據(jù)到Cache 中,此 Cache Line 中的數(shù)據(jù)也包含了變量 A 和 變量 B惧浴,此時(shí) 1 號(hào)和 2 號(hào)核?的Cache Line 狀態(tài)變?yōu)椤腹蚕頎顟B(tài)


image.png

④. 1 號(hào)核?需要修改變量 A存和,發(fā)現(xiàn)此 Cache Line 的狀態(tài)是「共享」?fàn)顟B(tài),所以先需要通過(guò)總線發(fā)送消息給 2 號(hào)核?衷旅,通知 2 號(hào)核?把 Cache 中對(duì)應(yīng)的 Cache Line 標(biāo)記為「已失效」?fàn)顟B(tài)捐腿,然后 1 號(hào)核?對(duì)應(yīng)的 Cache Line 狀態(tài)變成「已修改」?fàn)顟B(tài),并且修改變量 A

image.png

⑤. 之后柿顶,2 號(hào)核?需要修改變量 B茄袖,此時(shí) 2 號(hào)核?的 Cache 中對(duì)應(yīng)的 Cache Line 是已失效狀態(tài),另外由于 1 號(hào)核?的 Cache 也有此相同的數(shù)據(jù)嘁锯,且狀態(tài)為「已修改」?fàn)顟B(tài)宪祥,所以要先把 1 號(hào)核?的 Cache 對(duì)應(yīng)的 Cache Line 寫回到內(nèi)存聂薪,然后 2 號(hào)核?再?gòu)膬?nèi)存讀取 CacheLine ??的數(shù)據(jù)到 Cache 中,最后把變量 B 修改到 2 號(hào)核?的 Cache 中蝗羊,并將狀態(tài)標(biāo)記為「已修改」?fàn)顟B(tài)藏澳。

image.png

所以,可以發(fā)現(xiàn)如果 1 號(hào)和 2 號(hào) CPU 核?這樣持續(xù)交替的分別修改變量 A 和 B耀找,就會(huì)重復(fù)④ 和 ⑤ 這兩個(gè)步驟翔悠,Cache 并沒(méi)有起到緩存的效果,雖然變量 A 和 B 之間其實(shí)并沒(méi)有任何的關(guān)系野芒,但是因?yàn)橥瑫r(shí)歸屬于?個(gè) Cache Line 蓄愁,這個(gè) Cache Line 中的任意數(shù)據(jù)被修改后,都會(huì)相互影響狞悲,從?出現(xiàn) ④ 和 ⑤ 這兩個(gè)步驟因此撮抓,這種因?yàn)槎鄠€(gè)線程同時(shí)讀寫同?個(gè) Cache Line 的不同變量時(shí),?導(dǎo)致 CPU Cache 失
效的現(xiàn)象稱為偽共享(False Sharing)

2.3效诅、避免偽共享的方法

因此胀滚,對(duì)于多個(gè)線程共享的熱點(diǎn)數(shù)據(jù),即經(jīng)常會(huì)修改的數(shù)據(jù)乱投,應(yīng)該避免這些數(shù)據(jù)剛好在同?個(gè) Cache Line 中咽笼,否則就會(huì)出現(xiàn)為偽共享的問(wèn)題。接下來(lái)戚炫,看看在實(shí)際項(xiàng)?中是?什么?式來(lái)避免偽共享的問(wèn)題的剑刑。在 Linux 內(nèi)核中存在 __cacheline_aligned_in_smp 宏定義,是?于解決偽共享的問(wèn)題我們可以使?上?介紹的宏定義双肤,將 b 的地址設(shè)置為 Cache Line 對(duì)?地址施掏,如下


image.png

所以,避免 Cache 偽共享實(shí)際上是?空間換時(shí)間的思想茅糜,浪費(fèi)?部分 Cache 空間七芭,從?換來(lái)性能的提升
再回到我們文章開頭中的引入案例中蔑赘,就可以明白狸驳,為什么我們?cè)诘诙€(gè)測(cè)試案例中,性能會(huì)提升這么多缩赛。 其實(shí)就是因?yàn)槲覀冊(cè)赾acheline1前p0-p6,在cacheline2中也定義了p9~p15的變量耙箍。按照一個(gè)cacheline 64字節(jié)來(lái)計(jì)算,long類型的p0~p6 再加上cacheline1 剛好64字節(jié)酥馍,p9~p15加上cacheline2 也是64字節(jié)辩昆,這樣就可以保證這 cacheline1 和 cacheline2 一定是分布在 不同的cacheLine 中。
其實(shí)旨袒,拋開本文的案例汁针,我們依舊可以在一些開源的代碼中找到這樣的處理方法术辐,比如在 jdk1.8 版本中的高并發(fā)累加器LongAdder中,內(nèi)部cell元素就用了@sun.misc.Contended扇丛,這個(gè)注解原理就是通過(guò)繼承的方式來(lái)填充屬性术吗,保證多個(gè)變量分布在不同的cacheline中。此外帆精,在超高并發(fā)的Disruptor內(nèi)存隊(duì)列中 中有?個(gè) RingBuffer较屿,也是通過(guò)字節(jié)填充 + 繼承的?式,來(lái)避免偽共享的問(wèn)題卓练,有興趣的讀者可以自行翻閱源碼隘蝎。

image.png

3、總結(jié)

在實(shí)際生產(chǎn)項(xiàng)目中襟企,我們很難通過(guò)工具嗅探到cpu緩存?zhèn)喂蚕淼陌l(fā)生嘱么。但是該現(xiàn)象發(fā)生有一個(gè)共同特點(diǎn),就是多個(gè)線程共享的熱點(diǎn)數(shù)據(jù)顽悼,即經(jīng)常會(huì)修改的數(shù)據(jù)曼振,應(yīng)該避免這些數(shù)據(jù)剛好在同?個(gè) Cache Line 中。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末蔚龙,一起剝皮案震驚了整個(gè)濱河市冰评,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌木羹,老刑警劉巖甲雅,帶你破解...
    沈念sama閱讀 210,978評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異坑填,居然都是意外死亡抛人,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門脐瑰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)妖枚,“玉大人,你說(shuō)我怎么就攤上這事苍在【常” “怎么了?”我有些...
    開封第一講書人閱讀 156,623評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵忌穿,是天一觀的道長(zhǎng)抒寂。 經(jīng)常有香客問(wèn)我结啼,道長(zhǎng)掠剑,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,324評(píng)論 1 282
  • 正文 為了忘掉前任郊愧,我火速辦了婚禮朴译,結(jié)果婚禮上井佑,老公的妹妹穿的比我還像新娘。我一直安慰自己眠寿,他們只是感情好躬翁,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評(píng)論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著盯拱,像睡著了一般盒发。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上狡逢,一...
    開封第一講書人閱讀 49,741評(píng)論 1 289
  • 那天宁舰,我揣著相機(jī)與錄音,去河邊找鬼奢浑。 笑死蛮艰,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的雀彼。 我是一名探鬼主播壤蚜,決...
    沈念sama閱讀 38,892評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼徊哑!你這毒婦竟也來(lái)了袜刷?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,655評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤实柠,失蹤者是張志新(化名)和其女友劉穎水泉,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體窒盐,經(jīng)...
    沈念sama閱讀 44,104評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡草则,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蟹漓。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片炕横。...
    茶點(diǎn)故事閱讀 38,569評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖葡粒,靈堂內(nèi)的尸體忽然破棺而出份殿,到底是詐尸還是另有隱情,我是刑警寧澤嗽交,帶...
    沈念sama閱讀 34,254評(píng)論 4 328
  • 正文 年R本政府宣布卿嘲,位于F島的核電站,受9級(jí)特大地震影響夫壁,放射性物質(zhì)發(fā)生泄漏拾枣。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望梅肤。 院中可真熱鬧司蔬,春花似錦、人聲如沸姨蝴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)左医。三九已至授帕,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間浮梢,已是汗流浹背豪墅。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留黔寇,地道東北人偶器。 一個(gè)月前我還...
    沈念sama閱讀 46,260評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像缝裤,于是被迫代替她去往敵國(guó)和親屏轰。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容