Cache False Sharing 快取偽分享

Cache False Sharing 快取偽分享

Cache False Sharing 快取偽分享

如果要讓你的程式跑得更快,盲目的增加 CPU 有時不是一個很好的解法。

現在 CPU 速度已經非常的快了,再加上一台個人 PC 動不動就四核、八核起跳。

所以想要讓程式的執行效率往上提升,程式人員可以試著從另一個角度來切入,就是你能把接下來所需要的資料放到離你的 CPU 多近~~

簡單來說,當程式開始執行載入到主記憶體(DRAM)後,如果要開始執行某一行程式碼,並且會對某個變數開始做操作時。這些相關的資料會開始被移動到L1, L2 cache 裡面。

也就是當 CPU 需要這些資料時,第一步會先找尋離它最近的地方。

所以當 CPU REGISTER 裡面沒有的時後,就會開始往L1、L2來去找,如果有的話,就把它拿來用。反之,如果沒有的話,就開始往L3(如果有的話)或是主記憶體去找。

而一般的站台或程式,在實作的所謂快取資料,就是都放在主記憶體。

可是程式裡面如果需要大量的使用這些資料時,如果讓程式儘量在 L1、L2這些離 CPU 最近的地方就是一個學問了。

對程式而已,去存取放在主記憶體裡面的資料好像很快,但在硬體的世界裡,L1、L2、主記憶體,這些速度的差距完全是一種跳躍式的差距。

底下二段程式碼可以無聊試跑看看時間的差距有多少

static int[] sharedData = new int[2];

public Main(){
                            sharedData[0] = 0;
            sharedData[1] = 0;
            Task task1 = Task.Factory.StartNew(() =>
            {
                for (int i = 0; i < 1000000000; i++)
                {
                    sharedData[0]++;
                }
            });
            Task task2 = Task.Factory.StartNew(() =>
            {
                for (int i = 0; i < 1000000000; i++)
                {
                    sharedData[1]++;
                }
            });
            Task.WaitAll(task1, task2);
            Console.WriteLine("變數一:" + sharedData[0]);
            Console.WriteLine("變數二:" + sharedData[1]);
}
[StructLayout(LayoutKind.Explicit, Size = 128)]
        struct MyStruct
        {
            [FieldOffset(0)]
            public int Variable1;
            [FieldOffset(64)]
            public int Variable2;
        }

public Main(){
            MyStruct myStruct = new MyStruct();
            myStruct.Variable1 = 0;
            myStruct.Variable2 = 0;
            Task task1 = Task.Factory.StartNew(() =>
            {
                for (int i = 0; i < 1000000000; i++)
                {
                    myStruct.Variable1++;
                }
            });
            Task task2 = Task.Factory.StartNew(() =>
            {
                for (int i = 0; i < 1000000000; i++)
                {
                    myStruct.Variable2++;
                }
            });
            Task.WaitAll(task1, task2);
            System.Console.WriteLine("變數一:" + myStruct.Variable1);
            System.Console.WriteLine("變數二:" + myStruct.Variable2);
}

 

上面二段程式碼主要有二個重點。

第一:這二段程式的差別是第一段的程式碼,會一直不段的回主記憶體拿資料,然後再放回 CPU 處理。所以在速度上會比第二段慢非常多。

第二:為何第二段會不需要一直回主記憶體拿資料,是因為在 L1, L2 快取裡面資料是用 cache line 為一個單位。

cache line 就是造成「Cache False Sharing 快取偽分享」的主要原因。

用簡單的情境來說明。一個變數如果在多個 CPU 都要操作的情況下,如果變數被 CPU1 修改了,這時後 CPU2 裡面的這個變數會被標註為 dirty 髒資料。所以 CPU2 實際要用到時會再回主記憶體拿一次資料。

這時 cache line 的問題就來了,以我的電腦來說,在快取裡面一個 cache line 的大小是 64 bytes。但正常一個 int32 是不需要這麼大的位置來儲存。所以當一個 int32 變數被載入到 cache line 裡面時,其他的 bytes 就會被用來儲放其它的變數用。

所以以「第一段」程式碼來說,sharedData 這個變數有很大的機會是會讓二個 int32 都放在同一個 cache line。這就會導致二個 CPU 一直不斷的進進出出主記憶體。

而「第二段」程式碼的做法,就是強制讓一個 int32 的變數佔用 64 bytes ,也就是整個 cache line 都是同一個變數。這樣就能夠大幅減少進出主記憶體的次數了。