自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

與程序員相關(guān)的CPU緩存知識

新聞 前端
今天寫一篇關(guān)于 CPU Cache 相關(guān)的文章,這篇文章比較長,主要分成這么幾個(gè)部分:基礎(chǔ)知識、緩存的命中、緩存的一致性、相關(guān)的代碼示例和延伸閱讀。

  好久沒有寫一些微觀方面的文章了,今天寫一篇關(guān)于 CPU Cache 相關(guān)的文章,這篇文章比較長,主要分成這么幾個(gè)部分:基礎(chǔ)知識、緩存的命中、緩存的一致性、相關(guān)的代碼示例和延伸閱讀。其中會講述一些多核 CPU 的系統(tǒng)架構(gòu)以及其原理,包括對程序性能上的影響,以及在進(jìn)行并發(fā)編程的時(shí)候需要注意到的一些問題。這篇文章我會盡量地寫簡單和通俗易懂一些,主要是講清楚相關(guān)的原理和問題,而對于一些細(xì)節(jié)和延伸閱讀我會在文章最后會給出相關(guān)的資源。

  因?yàn)闊o論你寫什么樣的代碼都會交給 CPU 來執(zhí)行,所以,如果你想寫出性能比較高的代碼,這篇文章中提到的技術(shù)還是值得認(rèn)真學(xué)習(xí)的。

  基礎(chǔ)知識

  首先,我們都知道現(xiàn)在的 CPU 多核技術(shù),都會有幾級緩存,老的 CPU 會有兩級內(nèi)存(L1 和 L2),新的 CPU 會有三級內(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 級別的,L3 會是 MB 級別的。例如:Intel Core i7-8700K ,是一個(gè) 6 核的 CPU,每核上的 L1 是 64KB(數(shù)據(jù)和指令各 32KB),L2 是 256K,L3 有 12MB(我的蘋果電腦是 Intel Core i9-8950HK,和 Core i7-8700K 的 Cache 大小一樣)。

  我們的數(shù)據(jù)就從內(nèi)存向上,先到 L3,再到 L2,再到 L1,最后到寄存器進(jìn)行 CPU 計(jì)算。為什么會設(shè)計(jì)成三層?這里有下面幾個(gè)方面的考慮:

  • 一個(gè)方面是物理速度,如果要更大的容量就需要更多的晶體管,除了芯片的體積會變大,更重要的是大量的晶體管會導(dǎo)致速度下降,因?yàn)樵L問速度和要訪問的晶體管所在的位置成反比,也就是當(dāng)信號路徑變長時(shí),通信速度會變慢。這部分是物理問題。
  • 另外一個(gè)問題是,多核技術(shù)中,數(shù)據(jù)的狀態(tài)需要在多個(gè) CPU 中進(jìn)行同步,并且,我們可以看到,cache 和 RAM 的速度差距太大,所以,多級不同尺寸的緩存有利于提高整體的性能。

  這個(gè)世界永遠(yuǎn)是平衡的,一面變得有多光鮮,另一面也會變得有多黑暗。建立這么多級的緩存,一定就會引入其它的問題,這里有兩個(gè)比較重要的問題,

  • 一個(gè)是比較簡單的緩存的命中率的問題。
  • 另一個(gè)是比較復(fù)雜的緩存更新的一致性問題。

  尤其是第二個(gè)問題,在多核技術(shù)下,這就很像分布式的系統(tǒng)了,要對多個(gè)地方進(jìn)行更新。

  緩存的命中

  在說明這兩個(gè)問題之前。我們需要要解一個(gè)術(shù)語 Cache Line。緩存基本上來說就是把后面的數(shù)據(jù)加載到離自己近的地方,對于 CPU 來說,它是不會一個(gè)字節(jié)一個(gè)字節(jié)的加載的,因?yàn)檫@非常沒有效率,一般來說都是要一塊一塊的加載的,對于這樣的一塊一塊的數(shù)據(jù)單位,術(shù)語叫“Cache Line”,一般來說,一個(gè)主流的 CPU 的 Cache Line 是 64 Bytes(也有的 CPU 用 32Bytes 和 128Bytes),64Bytes 也就是 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ù)塊會拷貝到 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),最簡單的 hash table 就是做“求模運(yùn)算”,比如:我們的 L1 Cache 有 512 個(gè) Cache Line,那么,公式:(內(nèi)存地址 mod 512)* 64 就可以直接找到所在的 Cache 地址的偏移了。但是,這樣的方式需要我們的程序?qū)?nèi)存地址的訪問要非常地平均,不然沖突就會非常嚴(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。如下圖所示。

  對于 N-Way 組關(guān)聯(lián),可能有點(diǎn)不好理解,這里個(gè)例子,并多說一些細(xì)節(jié)(不然后面的代碼你會不能理解),Intel 大多數(shù)處理器的 L1 Cache 都是 32KB,8-Way 組相聯(lián),Cache Line 是 64 Bytes。這意味著,

  • 32KB 的可以分成,32KB / 64 = 512 條 Cache Line。
  • 因?yàn)橛?8 Way,于是會每一 Way 有 512 / 8 = 64 條 Cache Line。
  • 于是每一路就有 64 x 64 = 4096 Byts 的內(nèi)存。

  為了方便索引內(nèi)存地址,

  • Tag:每條 Cache Line 前都會有一個(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 里的偏移量

  如下圖所示:(更多的細(xì)節(jié)可以讀一下《Cache: a place for concealment and safekeeping》)

(圖片來自《Cache: a place for concealment and safekeeping》)

  這也意味著:

  • L1 Cache 可映射 36bits 的內(nèi)存地址,一共 2^36 = 64GB 的內(nèi)存
  • 因?yàn)橹灰^ 24bits 相同就會被映射到同一個(gè) Way 中,所以,基本上就是說,有2^12 = 4096 個(gè)地址會放在同一 Way 中。
  • 當(dāng) CPU 要訪問一個(gè)內(nèi)存的時(shí)候,通過這個(gè)內(nèi)存的前 24bits 和中間的 6bits 可以直接定位相應(yīng)的 Cache Line。

  此外,當(dāng)有數(shù)據(jù)沒有命中緩存的時(shí)候,CPU 就會以最小為 Cache Line 的單元向內(nèi)存更新數(shù)據(jù)。當(dāng)然,CPU 并不一定只是更新 64Bytes,因?yàn)樵L問主存是在是太慢了,所以,一般都會多更新一些。好的 CPU 會有一些預(yù)測的技術(shù),如果找到一種 pattern 的話,就會預(yù)先加載更多的內(nèi)存,包括指令也可以預(yù)加載。這叫 Prefetching 技術(shù) (參看,Wikipedia 的 Cache Prefetching 和 紐約州立大學(xué)的 Memory Prefetching)。比如,你在 for-loop 訪問一個(gè)連續(xù)的數(shù)組,你的步長是一個(gè)固定的數(shù),內(nèi)存就可以做到 prefetching。(注:指令也是以預(yù)加載的方式執(zhí)行,參看本站的《代碼執(zhí)行的效率》中的第三個(gè)示例)

  了解這些細(xì)節(jié),會有利于我們知道在什么情況下有可以導(dǎo)致緩存的失效。

  緩存的一致性

  對于主流的 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í)在是太慢了。

  好了,現(xiàn)在問題來了,如果有一個(gè)數(shù)據(jù) x 在 CPU 第 0 核的緩存上被更新了,那么其它 CPU 核上對于這個(gè)數(shù)據(jù) x 的值也要被更新,這就是緩存一致性的問題。(當(dāng)然,對于我們上層的程序我們不用關(guān)心 CPU 多個(gè)核的緩存是怎么同步的,這對上層的代碼來說都是透明的)

  一般來說,在 CPU 硬件上,會有兩種方法來解決這個(gè)問題。

  • Directory 協(xié)議。這種方法的典型實(shí)現(xiàn)是要設(shè)計(jì)一個(gè)集中式控制器,它是主存儲器控制器的一部分。其中有一個(gè)目錄存儲在主存儲器中,其中包含有關(guān)各種本地緩存內(nèi)容的全局狀態(tài)信息。當(dāng)單個(gè) CPU Cache 發(fā)出讀寫請求時(shí),這個(gè)集中式控制器會檢查并發(fā)出必要的命令,以在主存和 CPU Cache 之間或在 CPU Cache 自身之間進(jìn)行數(shù)據(jù)同步和傳輸。
  • Snoopy 協(xié)議。這種協(xié)議更像是一種數(shù)據(jù)通知的總線型的技術(shù)。CPU Cache 通過這個(gè)協(xié)議可以識別其它 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è)中心式的,會有性能瓶頸,而且會增加整體設(shè)計(jì)的復(fù)雜度。而 Snoopy 協(xié)議更像是微服務(wù)+消息通訊,所以,現(xiàn)在基本都是使用 Snoopy 的總線的設(shè)計(jì)。

  這里,我想多寫一些細(xì)節(jié),因?yàn)檫@種微觀的東西,不自然就就會更分布式系統(tǒng)相關(guān)聯(lián),在分布式系統(tǒng)中我們一般用 Paxos/Raft 這樣的分布式一致性的算法。而在 CPU 的微觀世界里,則不必使用這樣的算法,原因是因?yàn)?CPU 的多個(gè)核的硬件不必考慮網(wǎng)絡(luò)會斷會延遲的問題。所以,CPU 的多核心緩存間的同步的核心就是要管理好數(shù)據(jù)的狀態(tài)就好了。

  這里介紹幾個(gè)狀態(tài)協(xié)議,先從最簡單的開始,MESI 協(xié)議,這個(gè)協(xié)議跟那個(gè)著名的足球運(yùn)動員梅西沒什么關(guān)系,其主要表示緩存數(shù)據(jù)有四個(gè)狀態(tài):Modified(已修改), Exclusive(獨(dú)占的),Shared(共享的),Invalid(無效的)。

  這些狀態(tài)的狀態(tài)機(jī)如下所示(有點(diǎn)復(fù)雜,你可以先不看,這個(gè)圖就是想告訴你狀態(tài)控制有多復(fù)雜):

  下面是個(gè)示例(如果你想看一下動畫演示的話,這里有一個(gè)網(wǎng)頁(MESI Interactive Animations),你可以進(jìn)行交互操作,這個(gè)動畫演示中使用的 Write Through 算法):

當(dāng)前操作 CPU0 CPU1 Memory 說明
1) CPU0 read (x)  x=1 (E)   x=1 只有一個(gè) CPU 有 x 變量,
所以,狀態(tài)是 Exclusive
2) CPU1 read (x)  x=1 (S) x=1(S) x=1 有兩個(gè) CPU 都讀取 x 變量,
所以狀態(tài)變成 Shared
3) CPU0 write (x,9)  x=9 (M) x=1(I) x=1 變量改變,在 CPU0 中狀態(tài)
變成 Modified,在 CPU1 中
狀態(tài)變成 Invalid
4) 變量 x 寫回內(nèi)存  x=9 (M) X=1(I) x=9 目前的狀態(tài)不變
5) CPU1  read (x)  x=9 (S) x=9(S) x=9 變量同步到所有的 Cache 中,
狀態(tài)回到 Shared

  MESI 這種協(xié)議在數(shù)據(jù)更新后,會標(biāo)記其它共享的 CPU 緩存的數(shù)據(jù)拷貝為 Invalid 狀態(tài),然后當(dāng)其它 CPU 再次 read 的時(shí)候,就會出現(xiàn) cache miss 的問題,此時(shí)再從內(nèi)存中更新數(shù)據(jù)。從內(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ī)和演示示例我就不貼了,我們只需要理解 MOESI 協(xié)議允許 CPU Cache 間同步數(shù)據(jù),于是也降低了對內(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)存,F(xiàn)orward 狀態(tài)下的數(shù)據(jù)是 clean 的,可以丟棄而不用另行通知。

  需要說明的是,AMD 用 MOESI,Intel 用 MESIF。所以,F(xiàn) 狀態(tài)主要是針對 CPU L3 Cache 設(shè)計(jì)的(前面我們說過,L3 是所有 CPU 核心共享的)。(相關(guān)的比較可以參看 StackOverlow 上這個(gè)問題的答案)

  程序性能

  了解了我們上面的這些東西后,我們來看一下對于程序的影響。

  示例一

  首先,假設(shè)我們有一個(gè) 64M 長的數(shù)組,設(shè)想一下下面的兩個(gè)循環(huán):

  1. const int LEN = 64*1024*1024
  2. int *arr = new int[LEN]; 
  3.  
  4. for (int i = 0; i < LEN; i += 2) arr[i] *= i; 
  5.  
  6. 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 會以一個(gè) Cache Line 64Bytes 最小時(shí)單位加載,也就是 16 個(gè) 32bits 的整型,所以,無論你步長是 2 還是8,都差不多。而后面的乘法其實(shí)是不耗 CPU 時(shí)間的。

  示例二

  我們再來看一個(gè)與緩存命中率有關(guān)的代碼,我們以一定的步長increment 來訪問一個(gè)連續(xù)的數(shù)組。

  1. for (int i = 0; i < 10000000; i++) { 
  2.     for (int j = 0; j < size; j += increment) { 
  3.         memory[j] += j; 
  4.     } 

  我們測試一下,在下表中, 表頭是步長,也就是每次跳多少個(gè)整數(shù),而縱向是這個(gè)數(shù)組可以跳幾次(你可以理解為要幾條 Cache Line),于是表中的任何一項(xiàng)代表了這個(gè)數(shù)組有多少,而且步長是多少。比如:橫軸是 512,縱軸是4,意思是,這個(gè)數(shù)組有 4*512 = 2048 個(gè)長度,訪問時(shí)按 512 步長訪問,也就是訪問其中的這幾項(xiàng):[0, 512, 1024, 1536] 這四項(xiàng)。

  表中同的項(xiàng)是,是循環(huán) 1000 萬次的時(shí)間,單位是“微秒”(除以 1000 后是毫秒)

  1. | count |   1    |   16  |  512  | 1024  | 
  2. ------------------------------------------ 
  3. |     1 |  17539 | 16726 | 15143 | 14477 | 
  4. |     2 |  15420 | 14648 | 13552 | 13343 | 
  5. |     3 |  14716 | 14463 | 15086 | 17509 | 
  6. |     4 |  18976 | 18829 | 18961 | 21645 | 
  7. |     5 |  23693 | 23436 | 74349 | 29796 | 
  8. |     6 |  23264 | 23707 | 27005 | 44103 | 
  9. |     7 |  28574 | 28979 | 33169 | 58759 | 
  10. |     8 |  33155 | 34405 | 39339 | 65182 | 
  11. |     9 |  37088 | 37788 | 49863 |156745 | 
  12. |    10 |  41543 | 42103 | 58533 |215278 | 
  13. |    11 |  47638 | 50329 | 66620 |335603 | 
  14. |    12 |  49759 | 51228 | 75087 |305075 | 
  15. |    13 |  53938 | 53924 | 77790 |366879 | 
  16. |    14 |  58422 | 59565 | 90501 |466368 | 
  17. |    15 |  62161 | 64129 | 90814 |525780 | 
  18. |    16 |  67061 | 66663 | 98734 |440558 | 
  19. |    17 |  71132 | 69753 |171203 |506631 | 
  20. |    18 |  74102 | 73130 |293947 |550920 | 

  我們可以看到,從[9,1024] 以后,時(shí)間顯注上升。包括[17,512] 和 [18,512] 也顯注上升。這是因?yàn)?,我機(jī)器的 L1 Cache 是 32KB, 8 Way 的,前面說過,8 Way 的一個(gè)組有 64 個(gè) Cache Line,也就是 4096 個(gè)字節(jié),而 1024 個(gè)整型正好是 4096 Bytes,所以,一旦過了 8 Way + 4096 Bytes 這個(gè)界,每個(gè)步長都無法命中 L1 Cache,每次都是 Cache Miss,所以,導(dǎo)致訪問時(shí)間一下子就上升了。而 [16, 512]也是一樣的,其中的幾步開始導(dǎo)致 L1 Cache 開始失效。

  示例三

  接下來,我們再來看個(gè)示例。下面是一個(gè)二維數(shù)組的兩種遍歷方式,一個(gè)逐行遍歷,一個(gè)是逐列遍歷,這兩種方式在理論上來說,尋址和計(jì)算量都是一樣的,執(zhí)行時(shí)間應(yīng)該也是一樣的。

  1. const int row = 1024
  2. const int col = 512 
  3. int matrix[row][col]; 
  4.  
  5. //逐行遍歷 
  6. int sum_row=0
  7. for(int r=0; r<row; r++) { 
  8.     for(int c=0; c<col; c++){ 
  9.         sum_row += matrix[r]; 
  10.     } 
  11.  
  12. //逐列遍歷 
  13. int sum_col=0
  14. for(int c=0; c<col; c++) { 
  15.     for(int r=0; r<row; r++){ 
  16.         sum_col += matrix[r]; 
  17.     } 

  然而,并不是,在我的機(jī)器上,得到下面的結(jié)果。

  • 逐行遍歷:0.081ms
  • 逐列遍歷:1.069ms

  執(zhí)行時(shí)間有十幾倍的差距。其中的原因,就是逐列遍歷對于 CPU Cache 的運(yùn)作方式并不友好,所以,付出巨大的代價(jià)。

  示例四

  接下來,我們來看一下多核下的性能問題,參看如下的代碼。兩個(gè)線程在操作一個(gè)數(shù)組的兩個(gè)不同的元素(無需加鎖),線程循環(huán) 1000 萬次,做加法操作。在下面的代碼中,我高亮了一行,就是p2指針,要么是p[1],或是 p[18],理論上來說,無論訪問哪兩個(gè)數(shù)組元素,都應(yīng)該是一樣的執(zhí)行時(shí)間。

  1. void fn (int* data) { 
  2.     for(int i = 0; i < 10*1024*1024; ++i) 
  3.         *data += rand (); 
  4.   
  5. int p[32]; 
  6.   
  7. int *p1 = &p[0]; 
  8. int *p2 = &p[1]; // int *p2 = &amp;p[30]; 
  9.   
  10. thread t1(fn, p1); 
  11. thread t2(fn, p2); 

  然而,并不是,在我的機(jī)器上執(zhí)行下來的結(jié)果是:

  • 對于 p[0] 和 p[1] :560ms
  • 對于 p[0] 和 p[30]:104ms

  這是因?yàn)?nbsp;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 上,就會導(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ì)。下面的代碼中,我們?yōu)槊恳粋€(gè)線程傳入一個(gè) id ,然后通過這個(gè) id 來完成對應(yīng)數(shù)組段的統(tǒng)計(jì)任務(wù)。這樣可以加快整個(gè)處理速度。

  1. int total_size = 16 * 1024 * 1024//數(shù)組長度 
  2. int* test_data = new test_data[total_size]; //數(shù)組 
  3. int nthread = 6//線程數(shù)(因?yàn)槲业臋C(jī)器是 6 核的) 
  4. int result[nthread]; //收集結(jié)果的數(shù)組 
  5.   
  6. void thread_func (int id) { 
  7.     result[id] = 0
  8.     int chunk_size = total_size / nthread + 1
  9.     int start = id * chunk_size; 
  10.     int end = min (start + chunk_size, total_size); 
  11.   
  12.     for ( int i = start; i < end; ++i ) { 
  13.         if (test_data[i] % 2 != 0 ) ++result[id]; 
  14.     } 

  然而,在執(zhí)行過程中,你會發(fā)現(xiàn),6 個(gè)線程居然跑不過 1 個(gè)線程。因?yàn)楦鶕?jù)上面的例子你知道 result[] 這個(gè)數(shù)組中的數(shù)據(jù)在一個(gè) Cache Line 中,所以,所有的線程都會對這個(gè) Cache Line 進(jìn)行寫操作,導(dǎo)致所有的線程都在不斷地重新同步 result[] 所在的 Cache Line,所以,導(dǎo)致 6 個(gè)線程還跑不過一個(gè)線程的結(jié)果。這叫 False Sharing。

  優(yōu)化也很簡單,使用一個(gè)線程內(nèi)的變量。

  1. void thread_func (int id) { 
  2.     result[id] = 0;  
  3.     int chunk_size = total_size / nthread + 1
  4.     int start = id * chunk_size; 
  5.     int end = min (start + chunk_size, total_size); 
  6.   
  7.     int c = 0//使用臨時(shí)變量,沒有 cache line 的同步了 
  8.     for ( int i = start; i < end; ++i ) { 
  9.         if (test_data[i] % 2 != 0 ) ++c; 
  10.     } 
  11.     result[id] = c; 

  我們把兩個(gè)程序分別在 1 到 32 個(gè)線程上跑一下,得出的結(jié)果畫一張圖如下所示:

  上圖中,我們可以看到,灰色的曲線就是第一種方法,橙色的就是第二種(用局部變量的)方法。當(dāng)只有一個(gè)線程的時(shí)候,兩個(gè)方法相當(dāng),而且第二種方法還略差一點(diǎn),但是在線程數(shù)增加的時(shí)候的時(shí)候,你會發(fā)現(xiàn),第二種方法的性能提高的非???。直到到達(dá) 6 個(gè)線程的時(shí)候,開始變得穩(wěn)定(前面說過,我的 CPU 是 6 核的)。而第一種方法無論加多少線程也沒有辦法超過第二種方法。因?yàn)榈谝环N方法不是 CPU Cache 友好的。

  篇幅問題,示例就寫到這里,相關(guān)的代碼參看我的 Github 相關(guān)倉庫。

  

責(zé)任編輯:張燕妮 來源: coolshell.cn
相關(guān)推薦

2015-09-11 09:35:35

CPU

2019-10-23 08:54:38

程序員CPUALU

2009-07-15 09:29:24

Java程序員

2013-07-15 13:45:16

程序員

2013-08-20 09:33:59

程序員

2011-07-07 14:47:15

PHP

2020-03-09 11:14:25

程序員技術(shù)設(shè)計(jì)

2020-03-29 08:19:56

程序員代碼

2019-05-16 08:36:53

Eureka緩存網(wǎng)關(guān)

2016-01-05 10:30:59

后端程序員緩存原理

2018-05-31 15:22:53

程序員女程序男性程序員

2018-06-14 09:59:48

程序員代碼大公司

2015-03-10 14:28:46

程序員編程知識經(jīng)驗(yàn)總結(jié)

2011-05-13 14:34:02

程序員

2017-03-27 10:17:54

程序員工作學(xué)習(xí)

2014-09-01 14:31:11

2017-11-14 21:30:15

2012-11-22 14:00:26

程序員

2019-07-12 15:28:41

緩存數(shù)據(jù)庫瀏覽器

2012-03-06 09:22:46

程序員
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號