成人无码视频,亚洲精品久久久久av无码,午夜精品久久久久久毛片,亚洲 中文字幕 日韩 无码

資訊專欄INFORMATION COLUMN

從Java視角理解系統(tǒng)結(jié)構(gòu)(三)偽共享

asce1885 / 2155人閱讀

摘要:從視角理解系統(tǒng)結(jié)構(gòu)連載關(guān)注我的微博鏈接了解最新動(dòng)態(tài)從我的前一篇博文中我們知道了緩存及緩存行的概念同時(shí)用一個(gè)例子說明了編寫單線程代碼時(shí)應(yīng)該注意的問題下面我們討論更為復(fù)雜而且更符合現(xiàn)實(shí)情況的多核編程時(shí)將會(huì)碰到的問題這些問題更容易犯連包作者大師的

從Java視角理解系統(tǒng)結(jié)構(gòu)連載, 關(guān)注我的微博(鏈接)了解最新動(dòng)態(tài)

從我的前一篇博文中, 我們知道了CPU緩存及緩存行的概念, 同時(shí)用一個(gè)例子說明了編寫單線程Java代碼時(shí)應(yīng)該注意的問題. 下面我們討論更為復(fù)雜, 而且更符合現(xiàn)實(shí)情況的多核編程時(shí)將會(huì)碰到的問題. 這些問題更容易犯, 連j.u.c包作者Doug Lea大師的JDK代碼里也存在這些問題.

MESI協(xié)議及RFO請(qǐng)求

從前一篇我們知道, 典型的CPU微架構(gòu)有3級(jí)緩存, 每個(gè)核都有自己私有的L1, L2緩存. 那么多線程編程時(shí), 另外一個(gè)核的線程想要訪問當(dāng)前核內(nèi)L1, L2 緩存行的數(shù)據(jù), 該怎么辦呢?

有人說可以通過第2個(gè)核直接訪問第1個(gè)核的緩存行. 這是可行的, 但這種方法不夠快. 跨核訪問需要通過Memory Controller(見上一篇的示意圖), 典型的情況是第2個(gè)核經(jīng)常訪問第1個(gè)核的這條數(shù)據(jù), 那么每次都有跨核的消耗. 更糟的情況是, 有可能第2個(gè)核與第1個(gè)核不在一個(gè)插槽內(nèi).況且Memory Controller的總線帶寬是有限的, 扛不住這么多數(shù)據(jù)傳輸. 所以, CPU設(shè)計(jì)者們更偏向于另一種辦法: 如果第2個(gè)核需要這份數(shù)據(jù), 由第1個(gè)核直接把數(shù)據(jù)內(nèi)容發(fā)過去, 數(shù)據(jù)只需要傳一次。

那么什么時(shí)候會(huì)發(fā)生緩存行的傳輸呢? 答案很簡(jiǎn)單: 當(dāng)一個(gè)核需要讀取另外一個(gè)核的臟緩存行時(shí)發(fā)生.
但是前者怎么判斷后者的緩存行已經(jīng)被弄臟(寫)了呢?

下面將詳細(xì)地解答以上問題.

首先我們需要談到一個(gè)協(xié)議–MESI協(xié)議(鏈接). 現(xiàn)在主流的處理器都是用它來保證緩存的相干性和內(nèi)存的相干性. M,E,S和I代表使用MESI協(xié)議時(shí)緩存行所處的四個(gè)狀態(tài):

M(修改, Modified): 本地處理器已經(jīng)修改緩存行, 即是臟行, 它的內(nèi)容與內(nèi)存中的內(nèi)容不一樣. 并且此cache只有本地一個(gè)拷貝(專有).

E(專有, Exclusive): 緩存行內(nèi)容和內(nèi)存中的一樣, 而且其它處理器都沒有這行數(shù)據(jù)

S(共享, Shared): 緩存行內(nèi)容和內(nèi)存中的一樣, 有可能其它處理器也存在此緩存行的拷貝

I(無效, Invalid): 緩存行失效, 不能使用

上圖源自于內(nèi)核開發(fā)者Ulrich Drepper著名的What Every Programmer Should Know About Memory一書(下載), 簡(jiǎn)要地展示了緩存行的四種狀態(tài)轉(zhuǎn)換. 不過他的書中沒有說明白這四個(gè)狀態(tài)是怎么轉(zhuǎn)換的, 下面我用小段文字來說明一下.

初始?一開始時(shí), 緩存行沒有加載任何數(shù)據(jù), 所以它處于I狀態(tài).

本地寫(Local Write)如果本地處理器寫數(shù)據(jù)至處于I狀態(tài)的緩存行, 則緩存行的狀態(tài)變成M.

本地讀(Local Read)?如果本地處理器讀取處于I狀態(tài)的緩存行, 很明顯此緩存沒有數(shù)據(jù)給它. 此時(shí)分兩種情況:

其它處理器的緩存里也沒有此行數(shù)據(jù), 則從內(nèi)存加載數(shù)據(jù)到此緩存行后,
再將它設(shè)成E狀態(tài), 表示只有我一家有這條數(shù)據(jù), 其它處理器都沒有

其它處理器的緩存有此行數(shù)據(jù), 則將此緩存行的狀態(tài)設(shè)為S狀態(tài).

P.S.如果處于M狀態(tài)的緩存行, 再由本地處理器寫入/讀出, 狀態(tài)是不會(huì)改變的.

遠(yuǎn)程讀(Remote Read)?假設(shè)我們有兩個(gè)處理器c1和c2. 如果c2需要讀另外一個(gè)處理器c1的緩存行內(nèi)容, c1需要把它緩存行的內(nèi)容通過內(nèi)存控制器(Memory Controller)發(fā)送給c2, c2接到后將相應(yīng)的緩存行狀態(tài)設(shè)為S. 在設(shè)置之前, 內(nèi)存也得從總線上得到這份數(shù)據(jù)并保存.

遠(yuǎn)程寫(Remote Write)?其實(shí)確切地說不是遠(yuǎn)程寫, 而是c2得到c1的數(shù)據(jù)后, 不是為了讀, 而是為了寫. 也算是本地寫, 只是c1也擁有這份數(shù)據(jù)的拷貝, 這該怎么辦呢? c2將發(fā)出一個(gè)RFO(Request For Owner)請(qǐng)求, 它需要擁有這行數(shù)據(jù)的權(quán)限, 其它處理器的相應(yīng)緩存行設(shè)為I, 除了它自已, 誰不能動(dòng)這行數(shù)據(jù). 這保證了數(shù)據(jù)的安全, 同時(shí)處理RFO請(qǐng)求以及設(shè)置I的過程將給寫操作帶來很大的性能消耗.

以上只是列舉了一些狀態(tài)轉(zhuǎn)換, 為下文做鋪墊. 如果全部描述,需要非常大量的文字, 大家參考這張圖就知道原因了, 可以通過此圖了解MESI協(xié)議更詳細(xì)的信息.

偽共享

我們從上節(jié)知道, 寫操作的代價(jià)很高, 特別當(dāng)需要發(fā)送RFO消息時(shí). 我們編寫程序時(shí), 什么時(shí)候會(huì)發(fā)生RFO請(qǐng)求呢? 有以下兩種:

線程的工作從一個(gè)處理器移到另一個(gè)處理器, 它操作的所有緩存行都需要移到新的處理器上. 此后如果再寫緩存行, 則此緩存行在不同核上有多個(gè)拷貝, 需要發(fā)送RFO請(qǐng)求了.

兩個(gè)不同的處理器確實(shí)都需要操作相同的緩存行

由上一篇我們知道, 在Java程序中,數(shù)組的成員在緩存中也是連續(xù)的. 其實(shí)從Java對(duì)象的相鄰成員變量也會(huì)加載到同一緩存行中. 如果多個(gè)線程操作不同的成員變量, 但是相同的緩存行, 偽共享(False Sharing)問題就發(fā)生了. 下面引用Disruptor項(xiàng)目Lead的博文中的示例圖和實(shí)驗(yàn)例子(偷會(huì)懶,但會(huì)加上更詳細(xì)的profile方法).

一個(gè)運(yùn)行在處理器core 1上的線程想要更新變量X的值, 同時(shí)另外一個(gè)運(yùn)行在處理器core 2上的線程想要更新變量Y的值. 但是, 這兩個(gè)頻繁改動(dòng)的變量都處于同一條緩存行. 兩個(gè)線程就會(huì)輪番發(fā)送RFO消息, 占得此緩存行的擁有權(quán). 當(dāng)core 1取得了擁有權(quán)開始更新X, 則core 2對(duì)應(yīng)的緩存行需要設(shè)為I狀態(tài). 當(dāng)core 2取得了擁有權(quán)開始更新Y, 則core 1對(duì)應(yīng)的緩存行需要設(shè)為I狀態(tài)(失效態(tài)). 輪番奪取擁有權(quán)不但帶來大量的RFO消息, 而且如果某個(gè)線程需要讀此行數(shù)據(jù)時(shí), L1和L2緩存上都是失效數(shù)據(jù), 只有L3緩存上是同步好的數(shù)據(jù).從前一篇我們知道, 讀L3的數(shù)據(jù)非常影響性能. 更壞的情況是跨槽讀取, L3都要miss,只能從內(nèi)存上加載.

表面上X和Y都是被獨(dú)立線程操作的, 而且兩操作之間也沒有任何關(guān)系.只不過它們共享了一個(gè)緩存行, 但所有競(jìng)爭(zhēng)沖突都是來源于共享.

實(shí)驗(yàn)及分析

引用Martin的例子, 稍做修改,代碼如下:

public final class FalseSharing implements Runnable {
    public static int NUM_THREADS = 4; // change
    public final static long ITERATIONS = 500L * 1000L * 1000L;
    private final int arrayIndex;
    private static VolatileLong[] longs;

    public FalseSharing(final int arrayIndex) {
        this.arrayIndex = arrayIndex;
    }

    public static void main(final String[] args) throws Exception {
        Thread.sleep(10000);
        System.out.println("starting....");
        if (args.length == 1) {
            NUM_THREADS = Integer.parseInt(args[0]);
        }

        longs = new VolatileLong[NUM_THREADS];
        for (int i = 0; i < longs.length; i++) {
            longs[i] = new VolatileLong();
        }
        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 volatile long value = 0L;
        public long p1, p2, p3, p4, p5, p6; // 注釋
    }
}

代碼的邏輯是默認(rèn)4個(gè)線程修改一數(shù)組不同元素的內(nèi)容.? 元素的類型是VolatileLong, 只有一個(gè)長(zhǎng)整型成員value和6個(gè)沒用到的長(zhǎng)整型成員. value設(shè)為volatile是為了讓value的修改所有線程都可見. 在一臺(tái)Westmere(Xeon E5620 8core*2)機(jī)器上跑一下看

$ java FalseSharing
starting....
duration = 9316356836
$ java FalseSharing
starting....
duration = 59791968514

兩個(gè)邏輯一模一樣的程序, 前者只需要9秒, 后者跑了將近一分鐘, 這太不可思議了! 我們用偽共享(False Sharing)的理論來分析一下. 后面的那個(gè)程序longs數(shù)組的4個(gè)元素,由于VolatileLong只有1個(gè)長(zhǎng)整型成員, 所以整個(gè)數(shù)組都將被加載至同一緩存行, 但有4個(gè)線程同時(shí)操作這條緩存行, 于是偽共享就悄悄地發(fā)生了. 讀者可以測(cè)試一下2,4,8, 16個(gè)線程分別操作時(shí)分別是什么效果, 什么樣的趨勢(shì).

那么怎么避免偽共享呢? 我們未注釋的代碼就告訴了我們方法. 我們知道一條緩存行有64字節(jié), 而Java程序的對(duì)象頭固定占8字節(jié)(32位系統(tǒng))或12字節(jié)(64位系統(tǒng)默認(rèn)開啟壓縮, 不開壓縮為16字節(jié)), 詳情見?鏈接. 我們只需要填6個(gè)無用的長(zhǎng)整型補(bǔ)上6*8=48字節(jié), 讓不同的VolatileLong對(duì)象處于不同的緩存行, 就可以避免偽共享了(64位系統(tǒng)超過緩存行的64字節(jié)也無所謂,只要保證不同線程不要操作同一緩存行就可以). 這個(gè)辦法叫做補(bǔ)齊(Padding).

如何從系統(tǒng)層面觀察到這種優(yōu)化是切實(shí)有效的呢? 很可惜, 由于很多計(jì)算機(jī)的微架構(gòu)不同, 我們沒有工具來直接探測(cè)偽共享事件(包括Intel Vtune和Valgrind). 所有的工具都是從側(cè)面來發(fā)現(xiàn)的, 下面通過Linux利器OProfile來證明一下. 上面的程序的數(shù)組只是占64 * 4 = 256字節(jié), 而且在連續(xù)的物理空間, 照理來說數(shù)據(jù)會(huì)在L1緩存上就命中, 肯定不會(huì)傳入到L2緩存中, 只有在偽共享發(fā)生時(shí)才會(huì)出現(xiàn). 于是, 我們可以通過觀察L2緩存的IN事件就可以證明了,步驟如下:

# 設(shè)置捕捉L2緩存IN事件
$ sudo  opcontrol --setup --event=L2_LINES_IN:100000
# 清空工作區(qū)
$ sudo opcontrol --reset
# 開始捕捉
$ sudo opcontrol --start
# 運(yùn)行程序
$ java FalseSharing
# 程序跑完后, dump捕捉到的數(shù)據(jù)
$ sudo opcontrol --dump
# 停止捕捉
$ sudo opcontrol -h
# 報(bào)告結(jié)果
$ opreport -l `which java`

比較一下兩個(gè)版本的結(jié)果, 慢的版本:

$ opreport -l `which java`
CPU: Intel Westmere microarchitecture, speed 2400.2 MHz (estimated)
Counted L2_LINES_IN events (L2 lines alloacated) with a unit mask of 0x07 (any L2 lines alloacated) count 100000
samples  %        image name               symbol name
34085    99.8447  anon (tgid:18051 range:0x7fcdee53d000-0x7fcdee7ad000) anon (tgid:18051 range:0x7fcdee53d000-0x7fcdee7ad000)
51        0.1494  anon (tgid:16054 range:0x7fa485722000-0x7fa485992000) anon (tgid:16054 range:0x7fa485722000-0x7fa485992000)
2         0.0059  anon (tgid:2753 range:0x7f43b317e000-0x7f43b375e000) anon (tgid:2753 range:0x7f43b317e000-0x7f43b375e000)

快的版本:

$ opreport -l `which java`
CPU: Intel Westmere microarchitecture, speed 2400.2 MHz (estimated)
Counted L2_LINES_IN events (L2 lines alloacated) with a unit mask of 0x07 (any L2 lines alloacated) count 100000
samples  %        image name               symbol name
22       88.0000  anon (tgid:18873 range:0x7f3e3fa8a000-0x7f3e3fcfa000) anon (tgid:18873 range:0x7f3e3fa8a000-0x7f3e3fcfa000)
3        12.0000  anon (tgid:2753 range:0x7f43b317e000-0x7f43b375e000) anon (tgid:2753 range:0x7f43b317e000-0x7f43b375e000)

慢的版本由于False Sharing引發(fā)的L2緩存IN事件達(dá)34085次, 而快版本的為0次.

總結(jié)

偽共享在多核編程中很容易發(fā)生, 而且比較隱蔽. 例如, 在JDK的LinkedBlockingQueue中, 存在指向隊(duì)列頭的引用head和指向隊(duì)列尾的引用last. 而這種隊(duì)列經(jīng)常在異步編程中使有,這兩個(gè)引用的值經(jīng)常的被不同的線程修改, 但它們卻很可能在同一個(gè)緩存行, 于是就產(chǎn)生了偽共享. 線程越多, 核越多,對(duì)性能產(chǎn)生的負(fù)面效果就越大. 某些Java編譯器會(huì)將沒有使用到的補(bǔ)齊數(shù)據(jù), 即示例代碼中的6個(gè)長(zhǎng)整型在編譯時(shí)優(yōu)化掉, 可以在程序中加入一些代碼防止被編譯優(yōu)化。

public static long preventFromOptimization(VolatileLong v) {
    return v.p1 + v.p2 + v.p3 + v.p4 + v.p5 + v.p6;
}

另外, 由于Java的GC問題. 數(shù)據(jù)在內(nèi)存和對(duì)應(yīng)的CPU緩存行的位置有可能發(fā)生變化,
所以在使用pad的時(shí)候應(yīng)該注意GC的影響.

最后感謝同事撒迦,?長(zhǎng)仁在Java對(duì)象內(nèi)存布局及Profile工具上給予的幫助.

更新:

發(fā)現(xiàn)netty和grizzly的代碼中的LinkedTransferQueue中都使用了PaddedAtomicReference來代替原來的Node, 使用了補(bǔ)齊的辦法解決了隊(duì)列偽共享的問題. 不知道是不是JSR-166的人開發(fā)的, 看來他們?cè)缇鸵庾R(shí)到這個(gè)問題了. 但是從Doug Lea JSR-166的cvs看不到這個(gè)變化, 不知道究竟是誰改的? 他們的repository到底是在哪?

by MinZhou via ifeve

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://m.hztianpu.com/yun/64051.html

相關(guān)文章

  • Java視角理解系統(tǒng)結(jié)構(gòu)(二)CPU緩存

    摘要:從視角理解系統(tǒng)結(jié)構(gòu)連載關(guān)注我的微博鏈接了解最新動(dòng)態(tài)眾所周知是計(jì)算機(jī)的大腦它負(fù)責(zé)執(zhí)行程序的指令內(nèi)存負(fù)責(zé)存數(shù)據(jù)包括程序自身數(shù)據(jù)同樣大家都知道內(nèi)存比慢很多其實(shí)在年前的頻率和內(nèi)存總線的頻率在同一個(gè)級(jí)別訪問內(nèi)存只比訪問寄存器慢一點(diǎn)兒由于內(nèi)存的發(fā)展受到 從Java視角理解系統(tǒng)結(jié)構(gòu)連載, 關(guān)注我的微博(鏈接)了解最新動(dòng)態(tài) 眾所周知, CPU是計(jì)算機(jī)的大腦, 它負(fù)責(zé)執(zhí)行程序的指令; 內(nèi)存負(fù)責(zé)存數(shù)據(jù),...

    eternalshallow 評(píng)論0 收藏0
  • Java視角理解系統(tǒng)結(jié)構(gòu) (一) CPU上下文切換

    摘要:本文是從視角理解系統(tǒng)結(jié)構(gòu)連載文章在高性能編程時(shí)經(jīng)常接觸到多線程起初我們的理解是多個(gè)線程并行地執(zhí)行總比單個(gè)線程要快就像多個(gè)人一起干活總比一個(gè)人干要快然而實(shí)際情況是多線程之間需要競(jìng)爭(zhēng)設(shè)備或者競(jìng)爭(zhēng)鎖資源,導(dǎo)致往往執(zhí)行速度還不如單個(gè)線程在這里有一個(gè) 本文是從Java視角理解系統(tǒng)結(jié)構(gòu)連載文章 在高性能編程時(shí),經(jīng)常接觸到多線程. 起初我們的理解是, 多個(gè)線程并行地執(zhí)行總比單個(gè)線程要快, 就像多個(gè)...

    yuxue 評(píng)論0 收藏0
  • Java編程思想之多線程(一)

    摘要:多線程技術(shù)是個(gè)很龐大的課題,編程思想這本書英文版,以下簡(jiǎn)稱中也用了頁介紹的多線程體系。一個(gè)線程歸屬于唯一的進(jìn)程,線程無法脫離進(jìn)程而存在。五線程內(nèi)數(shù)據(jù)線程的私有數(shù)據(jù)僅歸屬于一個(gè)線程,不在線程之間共享,例如,,。 多線程技術(shù)是個(gè)很龐大的課題,《Java編程思想》這本書(英文版,以下簡(jiǎn)稱TIJ)中也用了136頁介紹Java的多線程體系。的確,Java語言發(fā)展到今天,多線程機(jī)制相比其他的語言從...

    taohonghui 評(píng)論0 收藏0
  • 死磕 java同步系列之volatile解析

    摘要:前半句是指線程內(nèi)表現(xiàn)為串行的語義,后半句是指指令重排序現(xiàn)象和工作內(nèi)存和主內(nèi)存同步延遲現(xiàn)象。關(guān)于內(nèi)存模型的講解請(qǐng)參考死磕同步系列之。目前國內(nèi)市面上的關(guān)于內(nèi)存屏障的講解基本不會(huì)超過這三篇文章,包括相關(guān)書籍中的介紹。問題 (1)volatile是如何保證可見性的? (2)volatile是如何禁止重排序的? (3)volatile的實(shí)現(xiàn)原理? (4)volatile的缺陷? 簡(jiǎn)介 volatile...

    番茄西紅柿 評(píng)論0 收藏0
  • 死磕 java同步系列之volatile解析

    摘要:前半句是指線程內(nèi)表現(xiàn)為串行的語義,后半句是指指令重排序現(xiàn)象和工作內(nèi)存和主內(nèi)存同步延遲現(xiàn)象。關(guān)于內(nèi)存模型的講解請(qǐng)參考死磕同步系列之。目前國內(nèi)市面上的關(guān)于內(nèi)存屏障的講解基本不會(huì)超過這三篇文章,包括相關(guān)書籍中的介紹。問題 (1)volatile是如何保證可見性的? (2)volatile是如何禁止重排序的? (3)volatile的實(shí)現(xiàn)原理? (4)volatile的缺陷? 簡(jiǎn)介 volatile...

    番茄西紅柿 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

閱讀需要支付1元查看
<