1-1趟济、CPU Cache 和緩存行(轉(zhuǎn))

轉(zhuǎn)自 CPU Cache與緩存行

image.png

引言

先看看下面這個(gè)循環(huán)遍歷哪個(gè)更快

int[][] array = new int[64 * 1024][1024];
        // 橫向遍歷
        long marked = System.currentTimeMillis();
        for (int i = 0; i < 64 * 1024; i++) {
            for (int j = 0; j < 1024; j++) {
                array[i][j]++;
            }
        }
        System.out.println("橫向遍歷 times:" + (System.currentTimeMillis() - marked) + "ms");

        marked = System.currentTimeMillis();
        // 縱向遍歷
        for (int i = 0; i < 1024; i++) {
            for (int j = 0; j < 64 * 1024; j++) {
                array[j][i]++;
            }
        }
        System.out.println("縱向遍歷 times:" + (System.currentTimeMillis() - marked) + "ms");

在 CPU處理器參數(shù)為 2.8 GHz Intel Core i7 的Mac上運(yùn)行的結(jié)果是

橫向遍歷 times:64ms
縱向遍歷 times:1689ms

Gallery of Processor Cache Effects 用7個(gè)源碼示例生動(dòng)的介紹 cache 原理织盼,深入淺出!但是可能因操作系統(tǒng)的差異、編譯器是否優(yōu)化火诸,以及近些年 cache 性能的提升,有些樣例在 Mac 的效果與原文相差較大荠察。

CPU Cache

CPU 訪問(wèn)內(nèi)存時(shí)置蜀,首先查詢(xún) cache 是否已緩存該數(shù)據(jù)。如果有悉盆,則返回?cái)?shù)據(jù)盯荤,無(wú)需訪問(wèn)內(nèi)存;如果不存在焕盟,則需把數(shù)據(jù)從內(nèi)存中載入 cache秋秤,最后返回給理器。在處理器看來(lái)脚翘,緩存是一個(gè)透明部件灼卢,旨在提高處理器訪問(wèn)內(nèi)存的速率,所以從邏輯的角度而言来农,編程時(shí)無(wú)需關(guān)注它鞋真,但是從性能的角度而言,理解其原理和機(jī)制有助于寫(xiě)出性能更好的程序沃于。Cache 之所以有效涩咖,是因?yàn)槌绦驅(qū)?nèi)存的訪問(wèn)存在一種概率上的局部特征:

  • Spatial Locality: 對(duì)于剛被訪問(wèn)的數(shù)據(jù)海诲,其相鄰的數(shù)據(jù)在將來(lái)被訪問(wèn)的概率高。
  • Temporal Locality: 對(duì)于剛被訪問(wèn)的數(shù)據(jù)檩互,其本身在將來(lái)被訪問(wèn)的概率高特幔。

以Mac OS 為例,可以使用命令 ysctl -a | grep hw 查詢(xún) cache 信息盾似,單位是字節(jié) Byte敬辣。

ysctl -a | grep hw

hw.cachelinesize: 64
hw.l1icachesize: 32768     一級(jí)指令緩存
hw.l1dcachesize: 32768    一級(jí)數(shù)據(jù)緩存
hw.l2cachesize: 262144
hw.l3cachesize: 6291456
  • CacheLine size : 64 Byte
  • L1 Data Cache:32 KB
  • L1 Instruction Cache:32 KB
  • L2 Cache:256KB
  • L3 Cache:6MB

下圖是計(jì)算機(jī)存儲(chǔ)的基本結(jié)構(gòu)。L1零院,L2,L3分別表示一級(jí)緩存村刨、二級(jí)緩存告抄、三級(jí)緩存。越靠近CPU的緩存嵌牺,速度越快打洼,容量也越小。L1緩存小但很快逆粹,并且緊靠著在使用它的CPU內(nèi)核募疮。分為指令緩存和數(shù)據(jù)緩存;L2大一些僻弹,也慢一些阿浓,并仍然只能被一個(gè)單獨(dú)的CPU核所使用;L3更大蹋绽、更慢芭毙,并且被單個(gè)插槽上所有的CPU核共享;最后是主存卸耘,由全部插槽上的所有CPU核共享退敦。

image.png

當(dāng)CPU執(zhí)行運(yùn)算的時(shí)候,它先去L1查找所需要的數(shù)據(jù)蚣抗、再去L2侈百、然后是L3,如果最后這些緩存中都沒(méi)有翰铡,所需的數(shù)據(jù)就要去主內(nèi)存拿钝域。走的越遠(yuǎn),運(yùn)算耗費(fèi)的時(shí)間就越長(zhǎng)两蟀。所以要盡量確保數(shù)據(jù)在L1緩存中网梢。

Martin和Mike的 QCon presentation 演講中給出了一些緩存未命中的消耗數(shù)據(jù),也就是從CPU訪問(wèn)不同層級(jí)數(shù)據(jù)的時(shí)間概念:

從CPU到 大約需要的CPU時(shí)鐘周期 大約需要的時(shí)間
主存 約60-80ns
QPI 總線傳輸(between sockets, not drawn) 約20ns
L3 cache 約40-45 cycles 約15ns
L2 cache 約10 cycles 約3ns
L1 cache 約3-4 cycles 約1ns
寄存器 1 cycle

從這里可以看出CPU讀取主存中的數(shù)據(jù)會(huì)比從L1中讀取慢了將近2個(gè)數(shù)量級(jí)赂毯。

我們?cè)诿扛?64 Byte (cache line size) 訪問(wèn)一次战虏,訪問(wèn)固定次數(shù)拣宰。隨著array的增大,看看能不能測(cè)試出 L1烦感,L2 和 L3 cache size的大醒采纭:

public static void main(String[] args) {
        for (int ARRAY_SIZE = 512; ARRAY_SIZE < 128 * 1024 * 1024; ARRAY_SIZE = ARRAY_SIZE << 1) {
            int steps = 640 * 1024 * 1024; // Arbitrary number of steps
            int length_mod = ARRAY_SIZE - 1;
            char[] arr = new char[ARRAY_SIZE];

            long marked = System.currentTimeMillis();
            for (int i = 0; i < steps; i += 64) {
                arr[i & length_mod]++; // (i & length_mod) is equal to (i % length_mod)
            }
            long used = (System.currentTimeMillis() - marked);
            System.out.println(formatSize(ARRAY_SIZE) + "\t" + used);
        }
    }

    /**
     * 把size單位轉(zhuǎn)化為KB, MB, GB
     */
    public static String formatSize(long size) {
        String hrSize = null;

        double b = size;
        double k = size / 1024.0;
        double m = ((size / 1024.0) / 1024.0);
        double g = (((size / 1024.0) / 1024.0) / 1024.0);
        double t = ((((size / 1024.0) / 1024.0) / 1024.0) / 1024.0);

        DecimalFormat dec = new DecimalFormat("0");

        if (t > 1) {
            hrSize = dec.format(t).concat(" TB");
        } else if (g > 1) {
            hrSize = dec.format(g).concat(" GB");
        } else if (m > 1) {
            hrSize = dec.format(m).concat(" MB");
        } else if (k > 1) {
            hrSize = dec.format(k).concat(" KB");
        } else {
            hrSize = dec.format(b).concat(" Bytes");
        }
        return hrSize;
    }

運(yùn)行結(jié)果如下:


image.png

可以看到32KB,256KB手趣,4MB之后耗時(shí)均有明顯上升晌该。

緩存行Cache Line

Cache是由很多個(gè) Cache line 組成的。Cache line 是 chche 和 RAM交換的最小單位绿渣,通常是 64 Byte朝群。當(dāng)CPU 把內(nèi)存的數(shù)據(jù)載入 cache時(shí),會(huì)把臨近的 64 Byte 的數(shù)據(jù)一同放入同一個(gè) Cache line 中符,因?yàn)榭臻g局部性姜胖;臨近的數(shù)據(jù)在將來(lái)被訪問(wèn)的可能性最大。

以大小為 32 KB 淀散,cache line 的大小為 64 Byte 的L1級(jí)緩存為例右莱,對(duì)于不同存放規(guī)則,其硬件圖簡(jiǎn)單表示一種設(shè)計(jì):


image.png

偽共享False Sharing

當(dāng)多線程修改互相獨(dú)立的變量時(shí)档插,如果這些變量共享同一個(gè)緩存行慢蜓,就會(huì)無(wú)意中影響彼此的性能,這就是偽共享郭膛。緩存行上的寫(xiě)競(jìng)爭(zhēng)是運(yùn)行在SMP系統(tǒng)中并行線程實(shí)現(xiàn)可伸縮性最重要的因素晨抡。有人將偽共享描述成無(wú)聲的性能殺手。


image.png

下面通過(guò)一段代碼饲鄙,看看偽共享對(duì)性能的影響凄诞。

/**
 * 偽共享
 */
public class FalseSharingNo implements Runnable {

    public final static long ITERATIONS = 500L * 1000L * 100L;

    private int arrayIndex = 0;

    private static ValuePadding[] longs;

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

    public static void main(final String[] args) throws Exception {
        for (int i = 1; i < 10; i++) {
            System.gc();
            final long start = System.currentTimeMillis();
            runTest(i);
            System.out.println(i + " Threads, duration = " + (System.currentTimeMillis() - start));
        }

    }

    private static void runTest(int NUM_THREADS) throws InterruptedException {
        Thread[] threads = new Thread[NUM_THREADS];
        longs = new ValuePadding[NUM_THREADS];
        for (int i = 0; i < longs.length; i++) {
            longs[i] = new ValuePadding();
        }
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(new FalseSharingNo(i));
        }

        for (Thread t : threads) {
            t.start();
        }

        for (Thread t : threads) {
            t.join();
        }
    }

    @Override
    public void run() {
        long i = ITERATIONS + 1;
        while (0 != --i) {
            longs[arrayIndex].value = 0L;
        }
    }

    public final static class ValuePadding {
        protected long p1, p2, p3, p4, p5, p6, p7;
        protected volatile long value = 0L;
        protected long p9, p10, p11, p12, p13, p14;
        protected long p15;
    }

    public final static class ValueNoPadding {
        // protected long p1, p2, p3, p4, p5, p6, p7;
        protected volatile long value = 0L;
        // protected long p9, p10, p11, p12, p13, p14, p15;
    }
}

在分別使用 ValuePadding 和 ValueNoPadding 兩種對(duì)象,讓多線程分別訪問(wèn)數(shù)組中相鄰的對(duì)象忍级,試圖構(gòu)建一個(gè)偽共享的場(chǎng)景帆谍。在有Padding填充的情況下,看看運(yùn)行結(jié)果:

1 Threads, duration = 390
2 Threads, duration = 402
3 Threads, duration = 418
4 Threads, duration = 442
5 Threads, duration = 467
6 Threads, duration = 537
7 Threads, duration = 591
8 Threads, duration = 648
9 Threads, duration = 730

把代碼中 ValuePadding 都替換為 ValueNoPadding 后的結(jié)果:

1 Threads, duration = 390
2 Threads, duration = 2016
3 Threads, duration = 2325
4 Threads, duration = 2644
5 Threads, duration = 3621
6 Threads, duration = 3805
7 Threads, duration = 3532
8 Threads, duration = 3612
9 Threads, duration = 2944

Cache Line 偽共享解決方案

處理偽共享的兩種方式:

  • 1轴咱、字節(jié)填充:增大元素的間隔汛蝙,使得不同線程存取的元素在不同的 cache line 上,典型的空間換時(shí)間朴肺。
  • 2窖剑、在每個(gè)線程中創(chuàng)建對(duì)應(yīng)元素的本地拷貝,結(jié)束后再寫(xiě)回全局?jǐn)?shù)組

我們這里只看第一種字節(jié)填充戈稿。保證不同線程的變量存在于不同的 Cache line 即可西土,這樣就不會(huì)出現(xiàn)偽貢獻(xiàn)問(wèn)題。在代碼層面如何實(shí)現(xiàn)圖中的字節(jié)填充呢鞍盗?

image.png

Java6 中實(shí)現(xiàn)字節(jié)填充

public class PaddingObject{
    public volatile long value = 0L;    // 實(shí)際數(shù)據(jù)
    public long p1, p2, p3, p4, p5, p6; // 填充
}

PaddingObject 類(lèi)中需要保存一個(gè)long類(lèi)型的value值需了,如果多線程操作同一個(gè)CacheLine中的PaddingObject對(duì)象跳昼,便無(wú)法發(fā)揮出 CPU Cache的優(yōu)勢(shì)(想象一下你定義了一個(gè) PaddingObject[] 數(shù)組,數(shù)組元素在內(nèi)存中連續(xù)肋乍,卻由于偽共享導(dǎo)致無(wú)法使用 CPU Cache 帶來(lái)的沮喪)鹅颊。

不知道你注意沒(méi)有,實(shí)際數(shù)據(jù) value + 用于填充的 p1-p6總共只占據(jù)了 7 * 8 = 56 個(gè)字節(jié)墓造,而 Cache Line 的大小應(yīng)該是64KB堪伍,這是有意為之,因?yàn)樵?Java中觅闽,對(duì)象頭還占據(jù)了 8個(gè)字節(jié)帝雇,所以一個(gè) PaddingObject 對(duì)象可以恰好占據(jù)一個(gè) Cache Line。

Java7 中實(shí)現(xiàn)字節(jié)填充

在 Java7 之后谱煤,一個(gè) JVM 的優(yōu)化給字節(jié)填充造成了一些影響摊求,上面的代碼片段 public long p1, p2, p3, p4, p5, p6; 會(huì)被認(rèn)為是無(wú)效代碼被優(yōu)化掉,有回歸到了偽共享的窘境之中刘离。

為了避免 JVM 的自動(dòng)優(yōu)化,需要使用繼承的方式來(lái)填充睹栖。

abstract class AbstractPaddingObject{
    protected long p1, p2, p3, p4, p5, p6;// 填充
}

public class PaddingObject extends AbstractPaddingObject{
    public volatile long value = 0L;    // 實(shí)際數(shù)據(jù)
}

Java8 中實(shí)現(xiàn)字節(jié)填充

//JDK 8中提供的注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface Contended {

    /**
     * The (optional) contention group tag.
     * This tag is only meaningful for field level annotations.
     *
     * @return contention group tag.
     */
    String value() default "";
}

在JDK 8里提供了一個(gè)新注解@Contented硫惕,可以用來(lái)減少 false sharing 的情況。JVM 在計(jì)算對(duì)象布局的時(shí)候就會(huì)自動(dòng)把標(biāo)注的字段拿出來(lái)并且插入合適的大小Padding野来。

因?yàn)檫@個(gè)功能暫時(shí)還是實(shí)驗(yàn)性功能恼除,暫時(shí)還沒(méi)到默認(rèn)普及給用戶(hù)代碼用的程度。要在用戶(hù)代碼(非Bootstrap class loader 或 extension class loader 所加載的類(lèi)) 中使用 @Contended 注解的話(huà)曼氛,需要使用 -XX:-RestrictContended 參數(shù)豁辉。

比如在JDK8中的 ConcurrentHashMap 源碼中,使用 @sun.misc.Contended 對(duì)靜態(tài)內(nèi)部類(lèi) CounterCell 進(jìn)行了修飾舀患。

/* ---------------- Counter support -------------- */

/**
 * A padded cell for distributing counts.  Adapted from LongAdder
 * and Striped64.  See their internal docs for explanation.
 */
@sun.misc.Contended 
static final class CounterCell {
        volatile long value;
        CounterCell(long x) { value = x; }
}

Thread

Thread 線程類(lèi)的源碼中徽级,使用 @sun.misc.Contended 對(duì)成員變量進(jìn)行修飾。

// The following three initially uninitialized fields are exclusively
// managed by class java.util.concurrent.ThreadLocalRandom. These
// fields are used to build the high-performance PRNGs in the
// concurrent code, and we can not risk accidental false sharing.
// Hence, the fields are isolated with @Contended.

/** The current seed for a ThreadLocalRandom */
@sun.misc.Contended("tlr")
long threadLocalRandomSeed;

/** Probe hash value; nonzero if threadLocalRandomSeed initialized */
@sun.misc.Contended("tlr")
int threadLocalRandomProbe;

/** Secondary seed isolated from public ThreadLocalRandom sequence */
@sun.misc.Contended("tlr")
int threadLocalRandomSecondarySeed;
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末聊浅,一起剝皮案震驚了整個(gè)濱河市餐抢,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌低匙,老刑警劉巖旷痕,帶你破解...
    沈念sama閱讀 218,451評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異顽冶,居然都是意外死亡欺抗,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門(mén)强重,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)绞呈,“玉大人贸人,你說(shuō)我怎么就攤上這事”ㄇ浚” “怎么了灸姊?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,782評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)秉溉。 經(jīng)常有香客問(wèn)我力惯,道長(zhǎng),這世上最難降的妖魔是什么召嘶? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,709評(píng)論 1 294
  • 正文 為了忘掉前任父晶,我火速辦了婚禮,結(jié)果婚禮上弄跌,老公的妹妹穿的比我還像新娘甲喝。我一直安慰自己,他們只是感情好铛只,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,733評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布埠胖。 她就那樣靜靜地躺著,像睡著了一般淳玩。 火紅的嫁衣襯著肌膚如雪直撤。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,578評(píng)論 1 305
  • 那天蜕着,我揣著相機(jī)與錄音谋竖,去河邊找鬼。 笑死承匣,一個(gè)胖子當(dāng)著我的面吹牛蓖乘,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播韧骗,決...
    沈念sama閱讀 40,320評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼嘉抒,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了宽闲?” 一聲冷哼從身側(cè)響起众眨,我...
    開(kāi)封第一講書(shū)人閱讀 39,241評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎容诬,沒(méi)想到半個(gè)月后娩梨,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,686評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡览徒,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,878評(píng)論 3 336
  • 正文 我和宋清朗相戀三年狈定,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,992評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡纽什,死狀恐怖措嵌,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情芦缰,我是刑警寧澤企巢,帶...
    沈念sama閱讀 35,715評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站让蕾,受9級(jí)特大地震影響浪规,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜探孝,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,336評(píng)論 3 330
  • 文/蒙蒙 一笋婿、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧顿颅,春花似錦缸濒、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,912評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至绍些,卻和暖如春讨永,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背遇革。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,040評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留揭糕,地道東北人萝快。 一個(gè)月前我還...
    沈念sama閱讀 48,173評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像著角,于是被迫代替她去往敵國(guó)和親揪漩。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,947評(píng)論 2 355