日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

CPU 如何讀寫數據的?

先來認識一下 CPU 的架構

 

一個 CPU 里通常會有多個 CPU 核心,并且每個 CPU 核心都有自己的 L1 Cache 和 L2 Cache,而 L1 Cache 通常分為(數據緩存)和(指令緩存),L3 Cache 則是多個核心共享的,這就是 CPU 典型的緩存層次。

上面提到的都是 CPU 內部的 Cache,放眼外部的話,還會有內存和硬盤,這些存儲設備共同構成了金字塔存儲層次。如下圖所示:

 

從上圖也可以看到,從上往下,存儲設備的容量會越大,而訪問速度會越慢。

CPU 訪問 L1 Cache 速度比訪問內存快 100 倍,這就是為什么 CPU 里會有 L1~L3 Cache 的原因,目的就是把 Cache 作為 CPU 與內存之間的緩存層,以減少對內存的訪問頻率。

CPU 從內存中讀取數據到 Cache 的時候,并不是一個字節一個字節讀取,而是一塊一塊的方式來讀取數據的,這一塊一塊的數據被稱為 CPU Cache Line(緩存塊),所以 CPU Cache Line 是 CPU 從內存讀取數據到 Cache 的單位

至于 CPU Cache Line 大小,在 linux 系統可以用下面的方式查看到,你可以看我服務器的 L1 Cache Line 大小是 64 字節,也就意味著 L1 Cache 一次載入數據的大小是 64 字節

 

那么對數組的加載, CPU 就會加載數組里面連續的多個數據到 Cache 里,因此我們應該按照物理內存地址分布的順序去訪問元素,這樣訪問數組元素的時候,Cache 命中率就會很高,于是就能減少從內存讀取數據的頻率, 從而可提高程序的性能。

但是,在我們不使用數組,而是使用單獨的變量的時候,則會有 Cache 偽共享的問題,Cache 偽共享問題上是一個性能殺手,我們應該要規避它。

接下來,就來看看 Cache 偽共享是什么?又如何避免這個問題?

Cache 偽共享

現在假設有一個雙核心的 CPU,這兩個 CPU 核心并行運行著兩個不同的線程,它們同時從內存中讀取兩個不同的數據,分別是類型為 long 的變量 A 和 B,這個兩個數據的地址在物理內存上是連續的,如果 Cahce Line 的大小是 64 字節,并且變量 A 在 Cahce Line 的開頭位置,那么這兩個數據是位于同一個 Cache Line 中,又因為 CPU Cache Line 是 CPU 從內存讀取數據到 Cache 的單位,所以這兩個數據會被同時讀入到了兩個 CPU 核心中各自 Cache 中。

 

我們來思考一個問題,如果這兩個不同核心的線程分別修改不同的數據,比如 1 號 CPU 核心的線程只修改了 變量 A,或 2 號 CPU 核心的線程的線程只修改了變量 B,會發生什么呢?

分析偽共享的問題

現在我們結合保證多核緩存一致的 MESI 協議,來說明這一整個的過程。

最開始變量 A 和 B 都還不在 Cache 里面,假設 1 號核心綁定了線程 A,2 號核心綁定了線程 B,線程 A 只會讀寫變量 A,線程 B 只會讀寫變量 B。

 

1 號核心讀取變量 A,由于 CPU 從內存讀取數據到 Cache 的單位是 Cache Line,也正好變量 A 和 變量 B 的數據歸屬于同一個 Cache Line,所以 A 和 B 的數據都會被加載到 Cache,并將此 Cache Line 標記為「獨占」狀態。

 

接著,2 號核心開始從內存里讀取變量 B,同樣的也是讀取 Cache Line 大小的數據到 Cache 中,此 Cache Line 中的數據也包含了變量 A 和 變量 B,此時 1 號和 2 號核心的 Cache Line 狀態變為「共享」狀態。

 

1 號核心需要修改變量 A,發現此 Cache Line 的狀態是「共享」狀態,所以先需要通過總線發送消息給 2 號核心,通知 2 號核心把 Cache 中對應的 Cache Line 標記為「已失效」狀態,然后 1 號核心對應的 Cache Line 狀態變成「已修改」狀態,并且修改變量 A。

 

之后,2 號核心需要修改變量 B,此時 2 號核心的 Cache 中對應的 Cache Line 是已失效狀態,另外由于 1 號核心的 Cache 也有此相同的數據,且狀態為「已修改」狀態,所以要先把 1 號核心的 Cache 對應的 Cache Line 寫回到內存,然后 2 號核心再從內存讀取 Cache Line 大小的數據到 Cache 中,最后把變量 B 修改到 2 號核心的 Cache 中,并將狀態標記為「已修改」狀態。

 

所以,可以發現如果 1 號和 2 號 CPU 核心這樣持續交替的分別修改變量 A 和 B,就會重復 ④ 和 ⑤ 這兩個步驟,Cache 并沒有起到緩存的效果,雖然變量 A 和 B 之間其實并沒有任何的關系,但是因為同時歸屬于一個 Cache Line ,這個 Cache Line 中的任意數據被修改后,都會相互影響,從而出現 ④ 和 ⑤ 這兩個步驟。

因此,當多線程修改互相獨立的變量時,如果這些變量共享同一個緩存行,就會無意中影響彼此的性能,這就是偽共享。

如何避免

舉個栗子

public class FalseSharingTest {
 
    public static void mAIn(String[] args) throws InterruptedException {
        testPointer(new Pointer());
    }
 
    private static void testPointer(Pointer pointer) throws InterruptedException {
        long start = System.currentTimeMillis();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100000000; i++) {
                pointer.x++;
            }
        });
 
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100000000; i++) {
                pointer.y++;
            }
        });
 
        t1.start();
        t2.start();
        t1.join();
        t2.join();
 
        System.out.println(System.currentTimeMillis() - start);
        System.out.println(pointer);
    }
}
 
class Pointer {
    volatile long x;
    volatile long y;
}

上面這個例子,我們聲明了一個Pointer的類,它包含了x和y兩個變量(必須聲明為volatile,保證可見性),一個線程對x進行自增1億次,一個線程對y進行自增1億次。

可以看到,x和y完全沒有任何關系,但是更新x的時候會把其它包含x的緩存行失效,同時y也就失效了,運行這段程序輸出的時間為3890ms。

偽共享的原理我們知道了,一個緩存行是64字節,一個long類型是8個字節,所以避免偽共享也很簡單,大概有以下三種方式:

(1)在兩個long類型的變量之間再加7個long類型

我們把上面的pointer改成下面這個結構

class Pointer {
    volatile long x;
    long p1, p2, p3, p4, p5, p6, p7;
    volatile long y;
}

再次運行程序,會發現輸出時間神奇的縮短為695ms

(2)重新創建自己的long類型,而不是JAVA自帶的long修改Pointer如下

class Pointer {
    MyLong x = new MyLong();
    MyLong y = new MyLong();
}
 
class MyLong {
    volatile long value;
    long p1, p2, p3, p4, p5, p6, p7;
}

同時把pointer.x++改為pointer.x.value++;等,再次運行程序發現時間是724ms,這樣本質上還是填充。所以,避免 Cache 偽共享實際上是用空間換時間的思想,浪費一部分 Cache 空間,從而換來性能的提升。

(3)使用@sun.misc.Contended注解(java8)

修改MyLong如下:

@sun.misc.Contended
class MyLong {
    volatile long value;
}

默認使用這個注解是無效的,需要在JVM啟動參數加上-XX:-RestrictContended才會生效,再次運行程序發現時間是718ms。注意,以上三種方式中的前兩種是通過加字段的形式實現的,加的字段又沒有地方使用,可能會被jvm優化掉,所以建議使用第三種方式。
Java 并發框架 Disruptor 使用「字節填充 + 繼承」的方式,來避免偽共享的問題。感興趣的同學可以自己去學習了解一下。

分享到:
標簽:共享
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定