日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 人文社科 > 生活经验 >内容正文

生活经验

CPU Cache原理与示例

發(fā)布時(shí)間:2023/11/28 生活经验 33 豆豆
生活随笔 收集整理的這篇文章主要介紹了 CPU Cache原理与示例 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

CPU Cache原理與示例
基礎(chǔ)知識(shí)
現(xiàn)在的 CPU 多核技術(shù),都會(huì)有幾級(jí)緩存,老的 CPU 會(huì)有兩級(jí)內(nèi)存(L1 和 L2),新的CPU會(huì)有三級(jí)內(nèi)存(L1,L2,L3 ),如下圖所示:

其中:
? L1 緩存分成兩種,一種是指令緩存,一種是數(shù)據(jù)緩存。L2 緩存和 L3 緩存不分指令和數(shù)據(jù)。
? L1 和 L2 緩存在每一個(gè) CPU 核中,L3 則是所有 CPU 核心共享的內(nèi)存。
? L1、L2、L3 的越離CPU近就越小,速度也越快,越離 CPU 遠(yuǎn),速度也越慢。
? 再往后面就是內(nèi)存,內(nèi)存的后面就是硬盤。看一些速度:
? L1 的存取速度:4 個(gè)CPU時(shí)鐘周期
? L2 的存取速度:11 個(gè)CPU時(shí)鐘周期
? L3 的存取速度:39 個(gè)CPU時(shí)鐘周期
? RAM內(nèi)存的存取速度 :107 個(gè)CPU時(shí)鐘周期
L1 的速度是 RAM 的 27 倍,但是 L1/L2 的大小基本上也就是 KB 級(jí)別的,L3 會(huì)是 MB 級(jí)別的。例如:Intel Core i7-8700K ,一個(gè) 6 核的 CPU,每核上的 L1 是 64KB(數(shù)據(jù)和指令各 32KB),L2 是 256K,L3 有 2MB(蘋果電腦是 Intel Core i9-8950HK,和Core i7-8700K 的Cache大小一樣)。
數(shù)據(jù)就從內(nèi)存向上,先到 L3,再到 L2,再到 L1,最后到寄存器進(jìn)行 CPU 計(jì)算。為什么會(huì)設(shè)計(jì)成三層?這里有下面幾個(gè)方面的考慮:
? 一個(gè)方面是物理速度,如果要更大的容量就需要更多的晶體管,除了芯片的體積會(huì)變大,更重要的是大量的晶體管會(huì)導(dǎo)致速度下降,因?yàn)樵L問速度和要訪問的晶體管所在的位置成反比,當(dāng)信號(hào)路徑變長(zhǎng)時(shí),通信速度會(huì)變慢。這是物理問題。
? 另外一個(gè)問題是,多核技術(shù)中,數(shù)據(jù)的狀態(tài)需要在多個(gè)CPU中進(jìn)行同步,cache 和RAM 的速度差距太大,多級(jí)不同尺寸的緩存有利于提高整體的性能。
? 這個(gè)世界永遠(yuǎn)是平衡的,一面變得有多光鮮,另一面也會(huì)變得有多黑暗。建立這么多級(jí)的緩存,一定就會(huì)引入其它的問題,這里有兩個(gè)比較重要的問題,
? 一個(gè)是比較簡(jiǎn)單的緩存的命中率的問題。
? 另一個(gè)是比較復(fù)雜的緩存更新的一致性問題。
尤其是第二個(gè)問題,在多核技術(shù)下,這就很像分布式的系統(tǒng)了,要對(duì)多個(gè)地方進(jìn)行更新。
緩存的命中
在說明這兩個(gè)問題之前。需要了解一個(gè)術(shù)語 Cache Line。緩存就是把后面的數(shù)據(jù)加載到離自己近的地方,對(duì)于 CPU 來說,不會(huì)一個(gè)字節(jié)一個(gè)字節(jié)的加載的,非常沒有效率,都是要一塊一塊的加載的,對(duì)于這樣的一塊一塊的數(shù)據(jù)單位,術(shù)語叫 Cache Line。
一般來說,一個(gè)主流的 CPU 的 Cache Line 是 64 Bytes(也有的CPU用32Bytes和128Bytes),64 Bytes也就是 16 個(gè) 32 位的整型,這就是 CPU 從內(nèi)存中撈數(shù)據(jù)的最小數(shù)據(jù)單位。
比如:Cache Line是最小單位(64Bytes),先把 Cache 分布多個(gè) Cache Line,比如:L1 有 32KB,32KB/64B = 512 個(gè) Cache Line。
一方面,緩存需要把內(nèi)存里的數(shù)據(jù)放進(jìn)來,英文叫 CPU Associativity。Cache 的數(shù)據(jù)放置的策略決定了內(nèi)存中的數(shù)據(jù)塊會(huì)拷貝到 CPU Cache 中的哪個(gè)位置上,因?yàn)?Cache 的大小遠(yuǎn)遠(yuǎn)小于內(nèi)存,需要有一種地址關(guān)聯(lián)的算法,能夠讓內(nèi)存中的數(shù)據(jù)可以被映射到 Cache 中來。這個(gè)有點(diǎn)像內(nèi)存地址從邏輯地址向物理地址映射的方法,但不完全一樣。
有如下的一些方法。
? 一種方法是,任何一個(gè)內(nèi)存地址的數(shù)據(jù)可以被緩存在任何一個(gè) Cache Line 里,這種方法是最靈活的,但是,如果要知道一個(gè)內(nèi)存是否存在于 Cache 中,就需要進(jìn)行 O(n) 復(fù)雜度的 Cache 遍歷,這是很沒有效率的。
? 另一種方法,為了降低緩存搜索算法,需要使用像Hash Table這樣的數(shù)據(jù)結(jié)構(gòu),最簡(jiǎn)單的hash table就是做求模運(yùn)算,比如: L1 Cache 有 512 個(gè) Cache Line,公式:(內(nèi)存地址 mod 512)* 64 就可以直接找到所在的Cache地址的偏移了。但是,這樣的方式需要程序?qū)?nèi)存地址的訪問要非常平均,不然沖突就會(huì)非常嚴(yán)重。這成了一種非常理想的情況了。
? 為了避免上述的兩種方案的問題,就要容忍一定的hash沖突,出現(xiàn)了 N-Way 關(guān)聯(lián)。把連續(xù)的N 個(gè) Cache Line 綁成一組,先把找到相關(guān)的組,再在這個(gè)組內(nèi)找到相關(guān)的 Cache Line。這叫 Set Associativity。如下圖所示。

對(duì)于 N-Way 組關(guān)聯(lián),可能有點(diǎn)不好理解,這里個(gè)例子,并多說一些細(xì)節(jié)(不然后面的代碼會(huì)不能理解),Intel 大多數(shù)處理器的 L1 Cache 都是 32KB,8-Way 組相聯(lián),Cache Line 是 64 Bytes。這意味著,
? 32KB的可以分成,32KB / 64 = 512 條 Cache Line。
? 因?yàn)橛? Way,于是會(huì)每一Way 有 512 / 8 = 64 條 Cache Line。
? 每一路就有 64 x 64 = 4096 Byts 的內(nèi)存。
? 為了方便索引內(nèi)存地址,
? Tag:每條 Cache Line 前都會(huì)有一個(gè)獨(dú)立分配的 24 bits來存的 tag,其就是內(nèi)存地址的前24bits
? Index:內(nèi)存地址后續(xù)的 6 個(gè) bits 則是在這一 Way 的是Cache Line 索引,2^6 = 64 剛好可以索引64條Cache Line
? Offset:再往后的 6bits 用于表示在 Cache Line 里的偏移量
如下圖所示:(圖片來自《Cache: a place for concealment and safekeeping》)
當(dāng)拿到一個(gè)內(nèi)存地址的時(shí)候,先拿出中間的 6bits,找到是哪組。

然后,在這一個(gè) 8 組的 cache line 中,再進(jìn)行 O(n) n=8 的遍歷,主是要匹配前 24bits 的 tag。如果匹配中了,就算命中,如果沒有匹配到,那就是 cache miss,如果是讀操作,就需要進(jìn)向后面的緩存進(jìn)行訪問了。
L2/L3 同樣是這樣的算法。淘汰算法有兩種,一種是隨機(jī)一種是 LRU。現(xiàn)在一般都是以 LRU 的算法(通過增加一個(gè)訪問計(jì)數(shù)器實(shí)現(xiàn))。

這也意味著:
? L1 Cache 可映射 36bits 的內(nèi)存地址,一共 2^36 = 64GB 的內(nèi)存
? 當(dāng) CPU 要訪問一個(gè)內(nèi)存的時(shí)候,通過這個(gè)內(nèi)存中間的 6bits 定位是哪個(gè) set,通過前 24bits 定位相應(yīng)的Cache Line。
? 就像一個(gè) hash Table 的數(shù)據(jù)結(jié)構(gòu)一樣,先是 O(1)的索引,然后進(jìn)入沖突搜索。
? 因?yàn)橹虚g的 6bits 決定了一個(gè)同一個(gè) set,所以,對(duì)于一段連續(xù)的內(nèi)存來說,每隔 4096 的內(nèi)存會(huì)被放在同一個(gè)組內(nèi),導(dǎo)致緩存沖突。
此外,當(dāng)有數(shù)據(jù)沒有命中緩存的時(shí)候,CPU 就會(huì)以最小為 Cache Line 的單元向內(nèi)存更新數(shù)據(jù)。當(dāng)然,CPU 并不一定只是更新 64Bytes,因?yàn)樵L問主存實(shí)在是太慢了,所以,一般都會(huì)多更新一些。好的 CPU 會(huì)有一些預(yù)測(cè)的技術(shù),如果找到一種 pattern 的話,就會(huì)預(yù)先加載更多的內(nèi)存,包括指令也可以預(yù)加載。
這叫 Prefetching 技術(shù) (參看,Wikipedia 的 Cache Prefetching 和 紐約州立大學(xué)的 Memory Prefetching)。比如,在for-loop訪問一個(gè)連續(xù)的數(shù)組,步長(zhǎng)是一個(gè)固定的數(shù),內(nèi)存就可以做到prefetching。(注:指令也是以預(yù)加載的方式執(zhí)行)
了解這些細(xì)節(jié),會(huì)有利于知道在什么情況下有可以導(dǎo)致緩存的失效。
緩存的一致性
對(duì)于主流的 CPU,緩存的寫操作基本上是兩種策略,
? 一種是 Write Back,寫操作只要在 cache 上,然后再 flush 到內(nèi)存上。
? 一種是 Write Through,寫操作同時(shí)寫到 cache 和內(nèi)存上。
主流的 CPU(如:Intel Core i7/i9)采用的是 Write Back 的策略,因?yàn)橹苯訉憙?nèi)存實(shí)在是太慢了。
如果有一個(gè)數(shù)據(jù) x 在 CPU 第 0 核的緩存上被更新了,其它 CPU 核上對(duì)于這個(gè)數(shù)據(jù) x 的值也要被更新,這就是緩存一致性的問題。(當(dāng)然,對(duì)于上層的程序不用關(guān)心 CPU 多個(gè)核的緩存是怎么同步的,這對(duì)上層的代碼來說都是透明的)
一般來說,在 CPU 硬件上,會(huì)有兩種方法來解決這個(gè)問題。
? Directory 協(xié)議。這種方法的典型實(shí)現(xiàn)是要設(shè)計(jì)一個(gè)集中式控制器,主存儲(chǔ)器控制器的一部分。其中有一個(gè)目錄存儲(chǔ)在主存儲(chǔ)器中,其中包含有關(guān)各種本地緩存內(nèi)容的全局狀態(tài)信息。當(dāng)單個(gè) CPU Cache 發(fā)出讀寫請(qǐng)求時(shí),這個(gè)集中式控制器會(huì)檢查并發(fā)出必要的命令,以在主存和 CPU Cache之間或在 CPU Cache自身之間進(jìn)行數(shù)據(jù)同步和傳輸。
? Snoopy 協(xié)議。這種協(xié)議更像是一種數(shù)據(jù)通知的總線型的技術(shù)。CPU Cache 通過這個(gè)協(xié)議可以識(shí)別其它Cache上的數(shù)據(jù)狀態(tài)。如果有數(shù)據(jù)共享的話,可以通過廣播機(jī)制將共享數(shù)據(jù)的狀態(tài)通知給其它 CPU Cache。這個(gè)協(xié)議要求每個(gè) CPU Cache 都可以窺探數(shù)據(jù)事件的通知并做出相應(yīng)的反應(yīng)。如下圖所示,有一個(gè) Snoopy Bus 的總線。

因?yàn)?Directory 協(xié)議是一個(gè)中心式的,會(huì)有性能瓶頸,會(huì)增加整體設(shè)計(jì)的復(fù)雜度。而 Snoopy 協(xié)議更像是微服務(wù)+消息通訊,所以,現(xiàn)在基本都是使用 Snoopy 的總線的設(shè)計(jì)。
在分布式系統(tǒng)中一般用 Paxos/Raft 這樣的分布式一致性的算法。
在 CPU 的微觀世界里,不必使用這樣的算法,因?yàn)?CPU 的多個(gè)核的硬件不必考慮網(wǎng)絡(luò)會(huì)斷會(huì)延遲的問題。所以,CPU 的多核心緩存間的同步的核心,就是要管理好數(shù)據(jù)的狀態(tài)。
這里介紹幾個(gè)狀態(tài)協(xié)議,先從最簡(jiǎn)單的開始,MESI 協(xié)議,這個(gè)協(xié)議跟那個(gè)著名的足球運(yùn)動(dòng)員梅西沒什么關(guān)系,其主要表示緩存數(shù)據(jù)有四個(gè)狀態(tài):Modified(已修改), Exclusive(獨(dú)占的),Shared(共享的),Invalid(無效的)。
這些狀態(tài)的狀態(tài)機(jī)如下所示(、這個(gè)圖就是想告訴狀態(tài)控制有多復(fù)雜):

下面是個(gè)示例(如果想看一下動(dòng)畫演示的話,這里有一個(gè)網(wǎng)頁(yè)(MESI Interactive Animations),可以進(jìn)行交互操作,這個(gè)動(dòng)畫演示中使用的 Write Through 算法):
MESI 這種協(xié)議在數(shù)據(jù)更新后,會(huì)標(biāo)記其它共享的 CPU 緩存的數(shù)據(jù)拷貝為 Invalid 狀態(tài),然后當(dāng)其它 CPU 再次read 的時(shí)候,就會(huì)出現(xiàn) cache miss 的問題,再?gòu)膬?nèi)存中更新數(shù)據(jù),意味著 20 倍速度的降低。
能不能直接從隔壁的 CPU 緩存中更新?可以,這就可以增加很多速度了,但是狀態(tài)控制也就變麻煩了。需要多來一個(gè)狀態(tài):Owner(宿主),用于標(biāo)記,更新數(shù)據(jù)源。出現(xiàn)了 MOESI 協(xié)議
MOESI 協(xié)議的狀態(tài)機(jī)和演示示例就不寫了(有興趣可以上Berkeley上看看相關(guān)的課件),只需要理解MOESI協(xié)議允許 CPU Cache 間同步數(shù)據(jù),降低了對(duì)內(nèi)存的操作,性能是非常大的提升,但是控制邏輯也非常復(fù)雜。
與 MOESI 協(xié)議類似的一個(gè)協(xié)議是 MESIF,其中的 F 是 Forward,同樣是把更新過的數(shù)據(jù)轉(zhuǎn)發(fā)給別的 CPU Cache。但是,MOESI 中的 Owner 狀態(tài) 和MESIF 中的 Forward 狀態(tài)有一個(gè)非常大的不一樣——Owner 狀態(tài)下的數(shù)據(jù)是 dirty 的,沒有寫回內(nèi)存,Forward 狀態(tài)下的數(shù)據(jù)是 clean的,可以丟棄,不用另行通知。
需要說明的是,AMD 用 MOESI,Intel 用 MESIF。F 狀態(tài)主要是針對(duì) CPU L3 Cache 設(shè)計(jì)的(L3 是所有 CPU 核心共享的)。
程序性能
看一下對(duì)于程序的影響。
示例一
首先,假設(shè)有一個(gè)64M長(zhǎng)的數(shù)組,設(shè)想一下下面的兩個(gè)循環(huán):
const int LEN = 6410241024;
int arr = new int[LEN];
for (int i = 0; i < LEN; i += 2)
arr[i] = i;
for (int i = 0; i < LEN; i += 8)
arr[i] = i;
按想法來看,第二個(gè)循環(huán)要比第一個(gè)循環(huán)少4倍的計(jì)算量,其應(yīng)該也是要快4倍的。但實(shí)際跑下來并不是,在機(jī)器上,第一個(gè)循環(huán)需要 127 毫秒,第二個(gè)循環(huán)則需要 121 毫秒,相差無幾。
這里最主要的原因就是 Cache Line,因?yàn)?CPU 會(huì)以一個(gè) Cache Line 64Bytes 最小時(shí)單位加載,也就是 16 個(gè) 32bits 的整型,所以,無論步長(zhǎng)是 2 還是 8,都差不多。后面的乘法其實(shí)是不耗 CPU 時(shí)間的。
示例二
再來看一個(gè)與緩存命中率有關(guān)的代碼,以一定的步長(zhǎng)increment 來訪問一個(gè)連續(xù)的數(shù)組。
for (int i = 0; i < 10000000; i++)
{
for (int j = 0; j < size; j += increment)
{
memory[j] += j;
}
}
測(cè)試一下,在下表中, 表頭是步長(zhǎng),也就是每次跳多少個(gè)整數(shù),而縱向是這個(gè)數(shù)組可以跳幾次(可以理解為要幾條 Cache Line),于是表中的任何一項(xiàng)代表了這個(gè)數(shù)組有多少,而且步長(zhǎng)是多少。
比如:橫軸是 512,縱軸是4,意思是,這個(gè)數(shù)組有 4
512 = 2048 個(gè)長(zhǎng)度,訪問時(shí)按512步長(zhǎng)訪問,也就是訪問其中的這幾項(xiàng):[0, 512, 1024, 1536] 這四項(xiàng)。
表中同的項(xiàng)是,是循環(huán) 1000 萬次的時(shí)間,單位是“微秒”(除以1000后是毫秒)
| count | 1 | 16 | 512 | 1024 | ------------------------------------------ | 1 | 17539 | 16726 | 15143 | 14477 | | 2 | 15420 | 14648 | 13552 | 13343 | | 3 | 14716 | 14463 | 15086 | 17509 | | 4 | 18976 | 18829 | 18961 | 21645 | | 5 | 23693 | 23436 | 74349 | 29796 | | 6 | 23264 | 23707 | 27005 | 44103 | | 7 | 28574 | 28979 | 33169 | 58759 | | 8 | 33155 | 34405 | 39339 | 65182 | | 9 | 37088 | 37788 | 49863 |156745 | | 10 | 41543 | 42103 | 58533 |215278 | | 11 | 47638 | 50329 | 66620 |335603 | | 12 | 49759 | 51228 | 75087 |305075 | | 13 | 53938 | 53924 | 77790 |366879 | | 14 | 58422 | 59565 | 90501 |466368 | | 15 | 62161 | 64129 | 90814 |525780 | | 16 | 67061 | 66663 | 98734 |440558 | | 17 | 71132 | 69753 |171203 |506631 | | 18 | 74102 | 73130 |293947 |550920 |
可以看到,從 [9,1024] 以后,時(shí)間顯著上升。包括 [17,512] 和 [18,512] 也顯著上升。這是因?yàn)?#xff0c;機(jī)器的 L1 Cache 是 32KB, 8 Way 的,前面說過,8 Way 的有 64 組,每組 8 個(gè) Cache Line,當(dāng) for-loop步長(zhǎng)超過 1024 個(gè)整型,也就是正好 4096 Bytes 時(shí),也就是導(dǎo)致內(nèi)存地址的變化是變化在高位的 24bits 上,
而低位的1 2bits 變化不大,尤其是中間6bits沒有變化,導(dǎo)致全部命中同一組 set,導(dǎo)致大量的 cache 沖突,導(dǎo)致性能下降,時(shí)間上升。[16, 512]也是一樣的,其中的幾步開始導(dǎo)致L1 Cache開始沖突失效。
示例三
接下來,再來看個(gè)示例。下面是一個(gè)二維數(shù)組的兩種遍歷方式,一個(gè)逐行遍歷,一個(gè)是逐列遍歷,這兩種方式在理論上來說,尋址和計(jì)算量都是一樣的,執(zhí)行時(shí)間應(yīng)該也是一樣的。
const int row = 1024; const int col = 512 int matrix[row][col]; //逐行遍歷 int sum_row=0; for(int _r=0; _r<row; _r++) { for(int _c=0; _c<col; _c++){ sum_row += matrix[_r][_c]; } } //逐列遍歷 int sum_col=0; for(int _c=0; _c<col; _c++) { for(int _r=0; _r<row; _r++){ sum_col += matrix[_r][_c]; } }
然而,并不是,在機(jī)器上,得到下面的結(jié)果。
? 逐行遍歷:0.081ms
? 逐列遍歷:1.069ms
執(zhí)行時(shí)間有十幾倍的差距。其中的原因,就是逐列遍歷對(duì)于 CPU Cache 的運(yùn)作方式并不友好,所以,付出巨大的代價(jià)。
示例四
接下來,來看一下多核下的性能問題,參看如下的代碼。兩個(gè)線程在操作一個(gè)數(shù)組的兩個(gè)不同的元素(無需加鎖),線程循環(huán)1000萬次,做加法操作。在下面的代碼中,高亮了一行,就是p2指針,要么是p[1],或是 p[30],理論上來說,無論訪問哪兩個(gè)數(shù)組元素,都應(yīng)該是一樣的執(zhí)行時(shí)間。
void fn (int
data) { for(int i = 0; i < 10
1024*1024; ++i) *data += rand(); } int p[32]; int *p1 = &p[0]; int *p2 = &p[1]; // int p2 = &p[30]; thread t1(fn, p1); thread t2(fn, p2);
然而,并不是,在機(jī)器上執(zhí)行下來的結(jié)果是:
? 對(duì)于 p[0] 和 p[1] :560ms
? 對(duì)于 p[0] 和 p[30]:104ms
這是因?yàn)?p[0] 和 p[1] 在同一條 Cache Line 上,而 p[0] 和 p[30] 則不可能在同一條Cache Line 上 ,CPU 的緩存最小的更新單位是 Cache Line,所以,這導(dǎo)致雖然兩個(gè)線程在寫不同的數(shù)據(jù),但是因?yàn)檫@兩個(gè)數(shù)據(jù)在同一條 Cache Line 上,就會(huì)導(dǎo)致緩存需要不斷進(jìn)在兩個(gè) CPU 的 L1/L2 中進(jìn)行同步,從而導(dǎo)致了 5 倍的時(shí)間差異。
示例五
接下來,再來看一下另外一段代碼:統(tǒng)計(jì)一下一個(gè)數(shù)組中的奇數(shù)個(gè)數(shù),但是這個(gè)數(shù)組太大了,希望可以用多線程來完成這個(gè)統(tǒng)計(jì)。下面的代碼中,為每一個(gè)線程傳入一個(gè) id ,然后通過這個(gè) id 來完成對(duì)應(yīng)數(shù)組段的統(tǒng)計(jì)任務(wù)。這樣可以加快整個(gè)處理速度。
int total_size = 16 * 1024 * 1024; //數(shù)組長(zhǎng)度
int
test_data = new test_data[total_size]; //數(shù)組
int nthread = 6; //線程數(shù)(因?yàn)闄C(jī)器是6核的)
int result[nthread]; //收集結(jié)果的數(shù)組
void thread_func (int id)
{
result[id] = 0;
int chunk_size = total_size / nthread + 1;
int start = id * chunk_size;
int end = min(start + chunk_size, total_size);
for ( int i = start; i < end; ++i )
{
if (test_data[i] % 2 != 0 )
++result[id];
}
}
然而,在執(zhí)行過程中,會(huì)發(fā)現(xiàn),6 個(gè)線程居然跑不過 1 個(gè)線程。因?yàn)楦鶕?jù)上面的例子知道 result[]數(shù)組中的數(shù)據(jù)在一個(gè) Cache Line 中,所以,所有的線程都會(huì)對(duì)這個(gè) Cache Line 進(jìn)行寫操作,導(dǎo)致所有的線程都在不斷地重新同步result[] 所在的 Cache Line,所以,導(dǎo)致 6 個(gè)線程還跑不過一個(gè)線程的結(jié)果。這叫False Sharing。
優(yōu)化也很簡(jiǎn)單,使用一個(gè)線程內(nèi)的變量。
void thread_func (int id)
{
result[id] = 0;
int chunk_size = total_size / nthread + 1;
int start = id * chunk_size;
int end = min(start + chunk_size, total_size);
int c = 0; //使用臨時(shí)變量,沒有cache line的同步了
for ( int i = start; i < end; ++i )
{
if (test_data[i] % 2 != 0 ) ++c;
}
result[id] = c;
}
把兩個(gè)程序分別在 1 到 32 個(gè)線程上跑一下,得出的結(jié)果畫一張圖如下所示(橫軸是線程數(shù),縱軸是完成統(tǒng)的時(shí)間,單位是微秒):

上圖中,可以看到,灰色的曲線就是第一種方法,橙色的就是第二種(用局部變量的)方法。當(dāng)只有一個(gè)線程的時(shí)候,兩個(gè)方法相當(dāng),基本沒有什么差別,但是在線程數(shù)增加時(shí),會(huì)發(fā)現(xiàn),第二種方法的性能提高的非常快。直到到達(dá) 6 個(gè)線程的時(shí)候,開始變得穩(wěn)定(前面說過,CPU 是6核的)。
第一種方法無論加多少線程也沒有辦法超過第二種方法。因?yàn)榈谝环N方法不是 CPU Cache 友好的。也就是說,第二種方法,只要 CPU 核數(shù)足夠多,就可以做到線性的性能擴(kuò)展,讓每一個(gè) CPU 核都跑起來,第一種則不能。

參考鏈接:
https://zhuanlan.zhihu.com/p/445961133
https://mp.weixin.qq.com/s/s9w–YRkyAvQi4LQcenq4g

總結(jié)

以上是生活随笔為你收集整理的CPU Cache原理与示例的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。