【轉】七個例子幫你更好地理解 CPU 緩存

我的大多數讀者都知道緩存是一種快速、小型、存儲最近已訪問的內存的地方。這個描述相當準確,但是深入處理器緩存如何工作的“枯燥”細節,會對嘗試理解程序性能有很大幫助。

在這篇博文中,我將通過示例代碼來說明緩存是如何工作的,以及它對現實世界中程序性能的影響。

雖然例子用的是 C#,但是不論哪種編程語言,對性能數據和最終結論的影響很小。

例1:內存訪問和性能

你預計運行 循環2 比 循環1 快多少?

1
2
3
4
5
6
7
8
9
int[] arr = new int[64 * 1024 * 1024];
// 循環1
for (int i = 0; i < arr.Length; i++)
????arr[i] *= 3;
// 循環2
for (int i = 0; i < arr.Length; i += 16)
????arr[i] *= 3;

第一個循環對數組中的每個元素都乘以 3,而第二個循環對每隔 16 個元素的數據乘以 3。第二個循環只做了第一個循環的大約6%的計算量,但是在現代計算機上,這兩個 for 循環運行的時間差不多相等:我電腦上分別是 80 和 78 毫秒。

這兩個循環耗費相同時間的原因與內存有關。這些循環的運行時間主要由訪問數組內存來決定,而不是整數乘法。并且我在例2中將解釋,硬件對這兩個循環執行相同的主存儲器訪問。

例2:緩存行(cache lines)的影響

(校對注:什么是 cache lines ?在內存和緩存直接傳輸的數據是大小固定的成塊數據,稱為?cache lines 。)

我們來深入地研究一下這個例子。我們嘗試1和16之外的其他步長:

1
2
for (int i = 0; i < arr.Length; i += K)
????arr[i] *= 3;

下面是這個循環運行不同步長(K)所花費的時間:

注意步長在1到16的范圍內時,循環的運行時間幾乎不變。但是從16開始,步長每增加一倍,其運行時間也減少一半。

其背后的原因是,如今的CPU并不是逐個字節地訪問內存。相反,它以(典型的)64字節的塊為單位取內存,稱作緩存行(cache lines)。當你讀取一個特定的內存地址時,整個緩存行都被從主內存取到緩存中。并且,此時讀取同一個緩存行中的其他數值非常快!

因為16個整數占用了64字節(一個緩存行),因此步長從1到16的for循環都必須訪問相同數量的緩存行:即數組中的所有緩存行。但是如果步長是32,我們只需要訪問約一半的緩存行;步長是64時,只有四分之一。

理解緩存行對特定類型的程序優化非常重要。例如,數據對齊可能會決定一個操作訪問一個還是兩個緩存行。如我們上面例子中看到的,它意味著在不對齊的情形下,操作將慢一倍。

例3:一級緩存(L1)和二級緩存(L2)的大小

如今的計算機都有兩級或者三級緩存,通常叫做L1,L2以及L3。如果你想知道不同緩存的大小,你可以使用SysInternals的CoreInfo工具,或者調用GetLogicalProcessorInfo Windows API。兩個方法都會告訴你各級緩存的大小,以及緩存行的大小。

在我電腦上,CoreInfo報告我有一個32KB的L1數據緩存,一個32KB的L1指令緩存,和一個4MB的L2數據緩存。L1緩存是每個核心獨享的,而每個L2緩存在兩個核心間共享:

1
2
3
4
5
6
7
8
9
10
11
Logical Processor to Cache Map: 邏輯處理器與緩存對應圖
*---? Data Cache????????? 0, Level 1,?? 32 KB, Assoc?? 8, LineSize? 64
*---? Instruction Cache?? 0, Level 1,?? 32 KB, Assoc?? 8, LineSize? 64
-*--? Data Cache????????? 1, Level 1,?? 32 KB, Assoc?? 8, LineSize? 64
-*--? Instruction Cache?? 1, Level 1,?? 32 KB, Assoc?? 8, LineSize? 64
**--? Unified Cache?????? 0, Level 2,??? 4 MB, Assoc? 16, LineSize? 64
--*-? Data Cache????????? 2, Level 1,?? 32 KB, Assoc?? 8, LineSize? 64
--*-? Instruction Cache?? 2, Level 1,?? 32 KB, Assoc?? 8, LineSize? 64
---*? Data Cache????????? 3, Level 1,?? 32 KB, Assoc?? 8, LineSize? 64
---*? Instruction Cache?? 3, Level 1,?? 32 KB, Assoc?? 8, LineSize? 64
--**? Unified Cache?????? 1, Level 2,??? 4 MB, Assoc? 16, LineSize? 64

讓我們通過實驗來核實一下。要做到這一點,我們將以16個整數為步長遍歷一個數組——這是修改每一個緩存行的一個簡單方法。當我們遍歷到最后一個值時,再回到開始向后遍歷。我們將實驗不同的數組長度,并且我們應該看到,每當數組長度超過一個緩存級別時,性能會隨著降低。

下面是程序:

1
2
3
4
5
6
int steps = 64 * 1024 * 1024; // Arbitrary number of steps
int lengthMod = arr.Length - 1;
for (int i = 0; i < steps; i++)
{
????arr[(i * 16) & lengthMod]++; // (x & lengthMod) is equal to (x % arr.Length)
}

下面是時間計時:

你可以看到在 32KB 和 4MB 后有明顯的下降——這正是我電腦上L1和L2緩存的大小。

例4:指令級并行

現在,讓我們看一些不一樣的東西。

下面這兩個循環中,你認為哪個會更快一些?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int steps = 256 * 1024 * 1024;
int[] a = new int[2];
// 循環1
for (int i=0; i < steps; i++) {
????a[0]++;
????a[0]++;
?}
// 循環2
for (int i=0; i < steps; i++) {
????a[0]++;
????a[1]++;
}

結果是,至少在我測試過的所有電腦上,第二個循環都比第一個循環快一倍。為什么呢?這與兩個循環主體中指令間的依賴關系有關。

在第一個循環主體中,指令間的依賴關系如下:

但是第二個循環中,依賴關系是這樣的:

現代處理器包含多個有并行機制的部件:它能同時讀取L1的兩個內存地址,或者同時運行兩條簡單的算數指令。在第一個循環內,處理器不能施展這種指令級并行;但是第二個循環中可以。

[更新]:reddit上很多人問編譯器優化的事情,以及是否能夠把 { a[0]++; a[0]++; } 優化成 { a[0]+=2; }。事實上,在涉及數組訪問時,C# 編譯器和 CLR JIT 不會做這個優化。我在 release 模式下(即包含優化選項)編譯了所有的例子,并在JIT之后的代碼中檢查是否有這個優化,但是沒有發現。

例5:緩存相關性

緩存設計的一個重要決策是,主存的每個塊是否能夠放入任何一個緩存槽,或某幾個緩存槽中的一個。

(譯者注:這里一個緩存槽和前面的緩存行相同;按照槽的大小,把主存分成若干塊,以塊為單位與緩存槽映射。下文提到的塊索引chunk index等于主存大小除以槽大小)。

把緩存槽映射到內存塊,有 3 種可選方案:

1. 直接映射緩存(Direct mapped cache)

每個內存塊只能存儲到一個緩存槽。一個簡單方案是通過塊索引把內存塊映射到緩存槽(塊索引 % 緩存槽數量(即取余數操作))。映射到同一個槽的內存塊不能同時存儲在緩存中。

2. N路關聯緩存(N-way set associative cache)

每個內存塊映射到N個特定緩存槽的任意一個槽。例如一個16路緩存,任何一個內存塊能夠被映射到16個不同的緩存槽。通常,具有相同低bit位地址的內存塊共享相同的16個槽。

3. 完全關聯緩存(Fully associative cache)

每個內存塊可以被映射到任意一個緩存槽(cache slot)。事實上,緩存操作和哈希表很像。

直接映射會遭遇沖突的問題——當多個塊同時競爭緩存的同一個槽時,它們不停地將對方踢出緩存,這將降低命中率。另一方面,完全關聯過于復雜,很難在硬件層面實現。N路關聯是典型的處理器緩存設計方案,因為它在實現難度和提高命中率之間做了良好的折衷。

例如,我電腦上的4M L2 緩存采用 16 路關聯的方案。所有的64字節大小的內存塊被分配到集合中(基于塊索引的低字節),同一個集合中的塊競爭使用 L2 緩存的16個槽。

由于 L2 緩存有65536 個槽,而每個集合需要16個槽,因此我們有4096個集合。由此,塊索引的低12比特能夠確定這個塊所在的集合(2^12 = 4096)。進而可以計算出,相差262144字節倍數的地址(4096*64)會競爭同一個槽。

為了使緩存相關性的影響表現出來,我需要重復地訪問同一個集合中的超過16個塊(譯者注:這樣16個緩存槽容納不下就會出現競爭)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static long UpdateEveryKthByte(byte[] arr, int K)
{
????Stopwatch sw = Stopwatch.StartNew();
????const int rep = 1024*1024; // Number of iterations – arbitrary
????int p = 0;
????for (int i = 0; i < rep; i++)
????{
????????arr[p]++;
????????p += K;
????????if (p >= arr.Length) p = 0;
????}
????sw.Stop();
????return sw.ElapsedMilliseconds;
}

這個方法對數組中每隔K個元素做遞增操作。當達到數組尾部時,再從頭開始。運行足夠多次后(2^20次),循環結束。

我使用不同尺寸的數組(每次遞增1MB大小),和不同的步長K,來運行UpdateEveryKthByte()。下面的圖呈現了結果,顏色越綠表示運行時間越長,顏色越白表示運行時間越短。

藍色區域(運行時間較長)部分表示當我們重復更新數組值時,這些值不能同時保存在緩沖中。比較亮的區域對應的運行時間約是80毫秒,接近白色的區域對飲運行時間約是10毫秒。

讓我來解釋一下圖中的藍色部分:

1. 為什么會出現豎直線?

豎直線對應的這些步長,在一次循環中訪問到的值跨越了同一個集合中的多個內存塊(大于16個)。對于這些步長訪問到的值,我電腦上的16路關聯緩存不能同時保存這些值。

一些糟糕的步長是2的冪次方:256和512。例如當數組是8MB,步長是512時。8MB的緩存行包含地址互相間隔262144字節倍數的32個值。由于512能夠整除262144,因此在一次循環內,這32個值都會被訪問到。

由于32大于16,因此這32個值將一直競爭緩存中相同的16個槽。

而一些不是2冪次方的值則是因為不夠幸運,它們剛好訪問到了同一個集合內的很多值。這些步長同樣會顯示成藍色線。

2. 為什么藍色線在4MB位置結束了呢?

當數組長度為4MB或者更小時,16路關聯緩存的表現和完全關聯緩存相同。

16路關聯緩存最多可以保存以262144字節長度分割的16個緩存行。在4MB中,由于16 * 262144 = 4194304 = 4MB,因此不會出現第17個或者更多個集合。

3. 為什么藍色的三角形位于左上角?

在三角形的區域,我們不能把所需的數據同時放入緩存——與緩存相關性無關,而與L2緩存大小有關系。

舉個數組長度為16MB、步長為128時的例子。我們重復地每隔128個字節更新數組中的值,即每次跨越了一個64字節的內存塊。對于16MB的數組,每隔一個塊存儲到緩存,這樣我們需要8MB大小的緩存。但是,我機器的緩存只有4MB。

即使我電腦上的4MB緩存使用完全關聯的方式,它仍然無法容納8MB的數據。

4. 為什么三角形最左側顏色變淡了呢?

注意變淡部分是從0開始,到64結束——正好是一個緩存行!正如例1和例2中解釋的,訪問同一個緩存行內的其他數據非常快。例如,當步長為16時,需要4步到達下一個緩存行。因此,這4次內存訪問的代價和1次訪問差不多。

由于對于所有用例,步數是相同,因此步數越少,運行時間越短。

當擴展這個圖時,規律是一樣的:

緩存相關性非常有趣,并且容易被證實,但是與本文中討論的其他問題相比,它并不是一個很大的問題。當你編寫程序時,它不應該是你首先要考慮的問題。

例6:緩存行共享假象

在多核機器上,緩存遇到了另一個問題——一致性。不同的核有完全獨立或者部分獨立的緩存。在我的電腦上,L1緩存是獨立的(這很常見);有兩組處理器,每組處理器共享一個L2緩存。具體來說,現代多核機器擁有多層次的緩存機制,其中更快和更小的緩存屬于獨立的處理器。

當一個處理器在它的緩存中修改一個值時,其他的處理器不能再使用舊的值了。在所有的緩存中,這個內存地址將變成無效地址。另外,由于緩存的粒度是緩存行,而不是單獨的字節,因此在所有緩存中的整個緩存行都變成無效!

為了演示這個問題,考慮下面的例子:

1
2
3
4
5
6
7
8
private static int[] s_counter = new int[1024];
private void UpdateCounter(int position)
{
????for (int j = 0; j < 100000000; j++)
????{
????????s_counter[position] = s_counter[position] + 3;
????}
}

在我的4核機器上,如果我在4個線程中調用UpdateCounter,參數分別是0、1、2、3,所有線程運行結束后花費的時間是4.3秒。

另一方面,如果我分別使用16、32、48、64的參數調用UpdateCounter,只花費了0.28秒!

為什么呢?在第一種情形下,所有的4個數據很可能位于同一個緩存行。內核每遞增一個數值,它就使包含這4個值的那個緩存行無效。其他所有內核訪問這個數值時,就會出現緩存未命中的情況。線程的這種行為使緩存失去了效果,消弱了程序的性能。

例7:硬件復雜性

即使你了解緩存工作的基本知識,但有時候硬件仍然會讓你驚訝。在優化措施、啟發式調度以及工作的細節上,不同的處理器存在差異。

在一些處理器上,當兩次訪問操作分別訪問不同的內存體(Memory Bank)時,L1緩存能夠并行執行這兩次訪問;而如果訪問相同的內存體,則會串行執行。同樣的,處理器的高級優化也會使你吃驚。例如,我過去在多臺電腦上運行過的“緩存行共享假象”例子,在我家里的電腦上需要微調代碼才能得到期望的結果——對于一些簡單的情況電腦能夠優化執行,以減少緩存失效。

下面是一個表明“硬件離奇性”的例子:

1
2
3
4
5
6
7
8
private static int A, B, C, D, E, F, G;
private static void Weirdness()
{
????for (int i = 0; i < 200000000; i++)
????{
????????<something>
????}
}

當我分別使用下面的三段不同代碼替換“”時,我得到下面的運行時間:

1
2
3
4
<something>?????????? Time
A++; B++; C++; D++; 719 ms
A++; C++; E++; G++; 448 ms
A++; C++;?????????? 518 ms

對A、B、C、D的遞增操作時間要比遞增A、C、E、G的時間長。更離奇的是,只遞增A和C使用了比遞增A、C、E、G更長的時間!

我并不清楚這些時間數字背后的原因,但是我猜測它與內存體(Memory Bank)有關。如果有人能夠解釋它的原因,我將非常愿意傾聽。

這個例子告訴我們,很難完全地預測硬件性能。你確實可以預測很多方面,但是最后,你需要測試并驗證你的預測結果,這非常重要。

結論

真心希望本文能夠幫助你理解緩存工作的細節,并在你的程序中應用這些知識。


來源:?<http://blog.jobbole.com/89759/>
?

來自為知筆記(Wiz)


轉載于:https://www.cnblogs.com/ssslinppp/p/4750310.html

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/258478.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/258478.shtml
英文地址,請注明出處:http://en.pswp.cn/news/258478.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Pytorch——對應點相乘和矩陣相乘

1. 點乘&#xff0c;對應元素相乘&#xff0c;不求和 import torcha torch.Tensor([[1,2], [3,4], [5,6]]) b1 a.mul(a)// b2a*a b1 Out[79]: tensor([[ 1., 4.],[ 9., 16.],[25., 36.]]) b2 Out[80]: tensor([[ 1., 4.],[ 9., 16.],[25., 36.]]) 以上兩種方法都可以表…

mysql初始化錯誤【一】Can't find error-message file '/usr/local/mysql/errmsg.sys'

環境&#xff1a;CentOS 7.2MySQL 5.7.18從mysql官方網站下載rpm包到服務器本地&#xff0c;依次安裝下面的RPM包&#xff1a;mysql-community-common-5.7.18-1.el7.x86_64.rpmmysql-community-server-5.7.18-1.el7.x86_64.rpmmysql-community-client-5.7.18-1.el7.x86_64.rpmm…

雙極型adc與stm32_關于STM32 雙ADC同步規則轉換兩路數據的問題?

因系統要求需升級ADC的采樣方式(以前方式&#xff1a;掃描方式&#xff0c;TIMER2觸發ADC軟啟動&#xff0c;2通道規則序列&#xff0c;DMA傳完中斷)&#xff0c;為了進一步實現兩路信號的同步性能&#xff0c;采樣STM32 雙ADC同步規則轉換。(timer2觸發ADC軟啟動&#xff0c;2…

面試金典--11.5

題目描述&#xff1a;給定排序后的字符串數組&#xff0c;中間有一些空串&#xff0c;要求找到給定字符串的位置 思路&#xff1a; &#xff08;1&#xff09;遍歷&#xff0c;最慢的 &#xff08;2&#xff09;二分查找&#xff0c;當mid處為空串&#xff0c;就找到最近的非空…

win10 平臺VS2019最簡安裝實現C++/C開發

這兩天一直在安裝vs2015,總是卡在visual studio 2015 出現安裝包丟失或損壞的現象&#xff0c;盡管按照網上很多方法嘗試解決&#xff0c;但是一直不行。算了。還是使用最新版的VS 2019安裝&#xff0c;沒想到很順利。 下面總結一下在win10平臺上最簡安裝VS2019&#xff0c;實…

Hook的兩個小插曲

看完了前面三篇文章后&#xff0c;這里我們來一個小插曲~~~~ 第一個小插曲。是前面文章一個CM精靈的分析。我們這里使用hook代碼來搞定。 第二個小插曲&#xff0c;是如今一些游戲&#xff0c;都有了支付上限&#xff0c;比如每天僅僅能花20塊錢來購買。好了。以下我們分開敘述…

### C++總結-[類成員函數]

C類中的常見函數。 #author: gr #date: 2015-07-23 #email: forgeruigmail.com 一、constructor, copy constructor, copy assignment, destructor 1. copy constructor必須傳引用&#xff0c;傳值編譯器會報錯 2. operator 返回值為引用&#xff0c;為了…

微信小程序和vue雙向綁定哪里不一樣_個人理解Vue和React區別

本文轉載自掘金&#xff0c;作者&#xff1a;binbinsilk&#xff0c;監聽數據變化的實現原理不同Vue 通過 getter/setter 以及一些函數的劫持&#xff0c;能精確知道數據變化&#xff0c;不需要特別的優化就能達到很好的性能React 默認是通過比較引用的方式進行的&#xff0c;如…

JS 省,市,區

1 // 純JS省市區三級聯動2 // 2011-11-30 by http://www.cnblogs.com/zjfree3 var addressInit function (_cmbProvince, _cmbCity, _cmbArea, defaultProvince, defaultCity, defaultArea) {4 var cmbProvince document.getElementById(_cmbProvince);5 var cmbCity…

使用極鏈/AutoDL云服務器復盤caffe安裝

繼上一次倒騰caffe安裝以后&#xff0c;因為博士畢業等原因&#xff0c;舊的服務器已經不能再使用&#xff0c;最近因論文等原因&#xff0c;不得不繼續來安裝一下我的caffe。這次運氣比較好&#xff0c;經歷了一晚上和一早上的痛苦之后&#xff0c;最終安裝成功了&#xff0c;…

ibatis中使用List作為傳入參數的使用方法及 CDATA使用

ibatis中list做回參很簡單&#xff0c;resultClass設為list中元素類型&#xff0c;dao層調用: (List)getSqlMapClientTemplate().queryForList("sqlName", paraName); 并經類型轉換即可&#xff0c;做入參還需要稍微調整下&#xff0c;本文主要講list做入參碰到的幾…

Samba服務

####################samba####################1.samba作用提供cifs協議實現共享文件2.安裝yum install samba samba-common samba-client -ysystemctl start smb nmbsystemctl enable smb nmb3.添加smb用戶smb用戶必須是本機用戶[rootlocalhost ~]# smbpasswd -a student New…

wpf 窗口的返回值_WPF Tips: Window.ShowDialog() 返回 true

Window.ShowDialog() 返回值為bool?。希望在窗口點擊OK時返回True。解決方法&#xff1a;ShowDialog()的注釋為&#xff1a;// Returns:// A System.Nullable value of type System.Boolean that specifies whether// the activity was accepted (true) or canceled (false). …

CodeForces 543D 樹形DP Road Improvement

題意&#xff1a; 有一顆樹&#xff0c;每條邊是好邊或者是壞邊&#xff0c;對于一個節點為x&#xff0c;如果任意一個點到x的路徑上的壞邊不超過1條&#xff0c;那么這樣的方案是合法的&#xff0c;求所有合法的方案數。 對于n個所有可能的x&#xff0c;輸出n個答案。 分析&am…

理解Javascritp中的引用

Author: bugall Wechat: bugallF Email: 769088641qq.com Github: https://github.com/bugall一&#xff1a; 函數中的引用傳遞 我們看下下面的代碼的正確輸出是什么 function changeStuff(a, b, c) {a a * 10;b.item "changed";c {item: "changed"}; …

通過擴展改善ASP.NET MVC的驗證機制[實現篇]

通過擴展改善ASP.NET MVC的驗證機制[實現篇] 原文:通過擴展改善ASP.NET MVC的驗證機制[實現篇]在《使用篇》中我們談到擴展的驗證編程方式&#xff0c;并且演示了本解決方案的三大特性&#xff1a;消息提供機制的分離、多語言的支持和多驗證規則的支持&#xff0c;我們現在來看…

canopen和1939區別_CAN 和 CANopen的區別和聯系

1、CAN與CANopen的共同點與不同點&#xff1a;CAN只定義了物理層與鏈路層&#xff0c;而沒有定義用戶層&#xff0c;用戶可根據自己的需要定義一些網絡上的通信約定&#xff1b; CANopen是在CAN的基礎上定義了用戶層&#xff0c;即規定了用戶、軟件、網絡終端等之間用來進行信…

ONOS系統架構演進,實現高可用性解決方案

上一篇文章《ONOS高可用性和可擴展性實現初探》講到了ONOS系統架構在高可用、可擴展方面技術概況&#xff0c;提到了系統在分布式集群中怎樣保證數據的一致性。在數據終于一致性方面&#xff0c;ONOS採用了Gossip協議。這一部分的變化不大&#xff0c;而在強一致性方案的選擇方…

Struts2_day01

Java Web開發常用框架 SSH(Struts2 Spring Hibernate)SSM(Struts2 Spring MyBatis)SSI(Struts2 Spring iBatis) 多種框架協同工作 Web層 -- Service層 -- Dao層 Struts2框架: Struts2是一個基于MVC設計模式的Web應用框架&#xff0c;它本質上相當于一個servlet&#xff0c;在MV…

使用 python 開發 Web Service

使用 python 開發 Web Service Python 是一種強大的面向對象腳本語言&#xff0c;用 python 開發應用程序往往十分快捷&#xff0c;非常適用于開發時間要求苛刻的原型產品。使用 python 開發 web service 同樣有語言本身的簡捷高速的特點&#xff0c;能使您快速地提供新的網絡服…