博文:https://chanjarster.github.io...git
原文:What every programmer should know about memory, Part 2: CPU caches程序員
關鍵詞:Cache prefetching、TLB cache missing、MESI protocol、Cache types(L1d、L1i、L二、L3)github
內存很慢,這就是爲什麼CPU cache存在的緣由,CPU cache內置在CPU內部,SRAM。
CPU cache尺寸不大。數組
CPU cache處於CPU和內存之間,默認狀況下CPU所讀寫的數據都存在cache中。緩存
Intel將CPU cache分爲data cache和code cache,這樣會有性能提高。安全
隨着CPU cache和內存的速度差別增大,在二者之間增長了更大可是更慢的CPU cache,爲什麼不擴大原CPU cache的尺寸?答案是不經濟。多線程
現代CPU core擁有三級緩存。架構
L1d是data cache,L1i是instruction cache(code cache)。上圖只是概要,現實中從CPU core到內存的數據流一路上能夠經過、也能夠不經過各個高層cache,這取決於CPU的設計者,這部分對於程序員是不可見的。併發
每一個處理器擁有多個core,每一個core幾乎擁有全部硬件資源的copy,每一個core能夠獨立運行,除非它們用到了相同的資源。
每一個core有用多個thread,每一個thread共享其所屬處理器的全部資源,Intel的thread僅對reigster作分離,甚至這個也是有限制的,有些register依然是共享的。dom
上面這張圖:
插播概念word:
Word,數據的天然單位,CPU指令集所能處理的數據單位。在x86-64架構中,word size=64 bits=8 bytes。
CPU cache中存儲的條目(entry)不是word,而是cache line,現在一條cache line大小爲64 bytes。每次從RAM中抓取數據的時候不只會將目標數據抓過來,還會將其附近的數據一併抓過來,構成64 bytes大小的cache line。
當一個cache line被修改了,可是尚未被寫到內存(main memory),則這個cache line被標記爲dirty。一旦被寫到內存,則dirty標記被清除。
對於多處理器系統,處理器之間會互相監視寫動做,並維持如下規則:
Cache eviction類型:
下表是Intel奔騰M處理訪問不一樣組件所需的CPU週期:
To Where | Cycles |
---|---|
Register | <= 1 |
L1d | ~3 |
L2 | ~14 |
Main Memory | ~240 |
下圖是寫不一樣尺寸數據下的性能表現:
根據經驗能夠推測出L1d size=2^12=4K,L2 size=2^20=1M。當數據<=4K時,正好可以放進L1d中,操做的CPU週期<10個。當數據>4K and <=1M時,會利用到L2,操做的CPU週期<75。當數據>1M時,CPU操做週期>400,則說明沒有L3,此時是直接訪問內存了。
很是重要:下面的例子裏CPU訪問數據是按照如下邏輯:
也就是說若是這個數據一開始在L1d、L2都不存在,那麼就得先從main memory加載到L2,而後從L2加載到L1d,最後CPU才能夠訪問。
沒看懂。略。
keyword:cache prefetching、TLB cache miss
測試方法是順序讀一個l
的數組:
struct l { struct l *n; long int pad[NPAD]; };
根據NPAD不一樣,元素的大小也不一樣:
被測CPU L1d cache=16K、L2 cache=1M、cache line=64 bytes。
下面是NPAD=0(element size=8 bytes,element間隔0 bytes),read 單個 element的平均時鐘週期:
Figure 3.10: Sequential Read Access, NPAD=0
上面的Working set size是指數組的總尺寸(bytes)。
能夠看到就算數據尺寸超過了16K(L1d size),對每一個元素的讀的CPU週期也沒有到達14,甚至當數據尺寸超過1M(L2 size)時也是這樣,這就是由於cache prefetching的功勞:
Cache prefetching是一項重要的優化手段,在使用連續的內存區域的時候,處理器會將後續數據預先加載到cache line中,也就是說當在訪問當前cache line的時候,下一個cache line的數據已經在半路上了。
Prefetching發生既會發生在L1中也會發生在L2中。
各個尺寸element的狀況:
Figure 3.11: Sequential Read for Several Sizes
觀察working set size <= L2,看(210~219區段):
這是爲何呢?此時prefetching沒有起到做用了嗎?這是由於:
仍是看各個尺寸element的狀況:
Figure 3.11: Sequential Read for Several Sizes
觀察working set size > L2,看(219以後的區段):
這是由於處理器從size of the strides判斷NPAD=15和31,小於prefetching window(具體後面會講),所以沒有啓用prefetching。而元素大小妨礙prefetching的硬件上的緣由是:prefetching沒法跨過page boundaries。
而NPAD=15與31的差異很大則是由於TLB cache miss。
TLB是用來存放virtual memory address到physical memory address的計算結果的(虛擬內存地址和物理內存地址在後面會講)。
測試NPAD=7(element size=64),每一個元素按照下列兩種方式排列的性能表現:
Figure 3.12: TLB Influence for Sequential Read
測試NPAD=1,element size=16 bytes,順序讀與寫:
Figure 3.13: Sequential Read and Write, NPAD=1
按照常理來講,Addnext0應該比較慢由於它作的工組比較多,然而在某些working set size下反而比Inc要好,這是由於:
下面這段沒看懂,也許這不重要。
The 「Addnext0」 test runs out of L2 faster than the 「Inc」 test, though. It needs more data loaded from main memory. This is why the 「Addnext0」 test reaches the 28 cycles level for a working set size of 2 21 bytes. The 28 cycles level is twice as high as the 14 cycles level the 「Follow」 test reaches. This is easy to explain, too. Since the other two tests modify memory an L2 cache eviction to make room for new cache lines cannot simply discard the data. Instead it has to be written to memory. This means the available bandwidth on the FSB is cut in half, hence doubling the time it takes to transfer the data from main memory to L2.
測試NPAD=15,element size=128 bytes。
Figure 3.14: Advantage of Larger L2/L3 Caches
因此緩存越大越能獲得性能上的提高。
以前已經看處處理器經過prefetching cache line到L2和L1d的方法,能夠隱藏main memory的訪問開銷,甚至L2的訪問開銷。可是,只有在內存訪問可預測的狀況下,這才能工做良好。
下圖是順序訪問和隨機訪問的對比:
Figure 3.15: Sequential vs Random Read, NPAD=0
後面的沒有看懂。
cache應該是coherent的,cache的coherency對於userlevel 代碼應該是徹底透明的,內核代碼除外。
若是一個cache line被修改了,那麼自此時間點以後的系統的結果和壓根沒有cache而且main memory被修改的結果是同樣。有兩個實現策略:
Write-through:
Write-back:
還有另外兩個策略,它們都用於地址空間的特殊區域,這些區域不禁實際的RAM支持。
Write-combining:
我的插播:
這個策略犧牲了必定的latency,可是提升了throughput,相似於批處理。
Uncacheable:
在多處理器系統和多核處理器中,對於全部不共享的cache,都會存在cache內容不一致的問題。兩個處理器之間不會共享L1d、L1i、L二、L3,同一處理器的兩個核之間至少不會共享L1d。
提供一條能從A處理器直接訪問B處理器的cache的通道是不切實際的,由於速度不夠快。因此比較實際的作法是將cache內容傳輸到其餘處理器以備不時之需。對於多核處理器也採用這種作法。
那何時傳輸呢?當一個處理器須要一條cache line作讀/寫,可是它在其餘處理器裏是dirty時。
那麼一個處理器是如何決定一條cache line在另一個處理器裏是否dirty呢?一般來講內存訪問是讀,而讀不會把一個cache line變成dirty。處理器每次對cache line的寫訪問以後都把cache line信息廣播出去是不切實際的。
開發了MESI緩存協同協議(MESI cache coherency protocol),規定了一條cache line的狀態有四種:Modified、Exclusive、Shared、Invalid。
MESI所解決的問題和分佈式緩存中數據同步的問題是一致的,好好看看,這可以帶來一些啓發
使用這四個狀態有可能有效率地實現write-back策略,同時支持多處理器併發使用read-only數據。
Figure 3.18: MESI Protocol Transitions
下面是四種狀態變化的解讀:
Invalid:
若是數據加載到cache line的目的是爲了讀,
Modified
若是B處理器要讀A處理器的Modified cache line,則A處理器必須將內容發送給B處理器,而後狀態變成Shared。
若是B處理器要寫A處理器的Modified cache line,則A處理器要把數據傳給B,而後標記爲Invalid。
Shared
若是本地寫一條Shared cache line,則變成Modified。
Exclusive
因此處理器會盡量把多的cache line維持在Exclusive狀態,而不是Shared狀態。
因此在多處理器系統中,除了填充cache line以外,咱們還得關注RFO消息對性能的影響。只要出現了RFO消息,就會變慢。
有兩種場景RFO消息是必須的:
同一條cache line是真的被兩個處理器須要。
影響Coherency protocol速度的因素:
用以前相同的程序測試多線程的表現,採用用例中最快的線程的數據。全部的處理器共享同一個bus到memory controller,而且只有一條到memory modules的bus。
Figure 3.19: Sequential Read Access, Multiple Threads
這個測試裏沒有修改數據,全部cache line都是shared,沒有發生RFO。可是即使如此2線程的時候有18%的性能損失,在4線程的時候則是34%。那麼可能的緣由就只多是一個或兩個瓶頸所形成的:處理器到memory controller的bus、memory controller到memory modules的bus。一旦working set超過L3尺寸以後,就要從main memory prefetch數據了,帶寬就不夠用了
這個測試用的是 「Sequential Read and Write, NPAD=1,Inc」,會修改內存。
Figure 3.20: Sequential Increment, Multiple Threads
注意圖中的Y軸不是線性增長的,因此看上去很小的差別實際上差異很大。
2線程依然有18%的性能損失,而4線程則有93%的性能損失,這意味4線程的時候prefetch流量核write-back流量飽和了bus。
圖中也能夠發現只要有多個線程,L1d基本上就很低效了。
L2倒不像L1d,彷佛沒有什麼影響。這個測試修改了內存,咱們預期會有不少RFO消息,可是並無看見二、4線程相比單線程有什麼性能損失。這是由於測試程序的關係。
下面這張圖主要是爲了展示使人吃驚的高數字,在極端狀況下處理list中的單個元素竟然要花費1500個週期。
Figure 3.21: Random Addnextlast, Multiple Threads
把case 一、二、3中的最大working set size的值總結出多線程效率:
#Threads | Seq Read | Seq Inc | Rand Add |
---|---|---|---|
2 | 1.69 | 1.69 | 1.54 |
4 | 2.98 | 2.07 | 1.65 |
Table 3.3: Efficiency for Multiple Threads
這個表顯示了在最大working set size,使用多線程能得到的可能的最好的加速。理論上2線程應該加速2,4線程應該加速4。好好觀察這個表裏的數字和理論值的差別。
下面這張圖顯示了Rand Add測試,在不一樣working set size下,多線程的加速效果:
Figure 3.22: Speed-Up Through Parallelism
L1d尺寸的測試結果,在L2和L3範圍內,加速效果基本上是線性的,一旦當尺寸超過L3時,數字開始下墜,而且2線程和4線程的數字下墜到同一點上。這也就是爲何很難看到大於4個處理器的系統使用同一個memory controller,這些系統必須採用不一樣的構造。
不一樣狀況下上圖的數字是不同的,這個取決於程序究竟是怎麼寫的。在某些狀況下,就算working set size能套進最後一級cache,也沒法得到線性加速。可是另外一方面依然有可能在大於兩個線程、更大working set size的狀況下得到線性加速。這個須要程序員作一些考量,後面會講。
Hyper-Threads(有時候也被稱爲Symmetric Multi-Threading, SMT),是CPU實現的一項技術,同時也是一個特殊狀況,由於各個線程並不可以真正的同時運行。超線程共享了CPU的全部資源除了register。各個core和CPU依然是並行運行的,可是hyper-threads不是。CPU負責hyper-threads的分時複用(time-multiplexing),噹噹前運行的hyper-thread發生延遲的時候,就調度令一個hyper-thread運行,而發生延遲的緣由大部分都是因內存訪問致使的。
當程序的運行2線程在一個hyper-thread核的時候,只有在如下狀況纔會比單線程更有效率:2個線程的運行時間之和低於單線程版本的運行時間。這是由於當一個線程在等待內存的時候能夠安排另外一個線程工做,而本來這個是串形的。
一個程序的運行時間大體能夠用下面這個簡單模型+單級cache來計算:
Texe = N[(1-Fmem)Tproc + Fmem(GhitTcache + (1-Ghit)Tmiss)]
爲了使用兩個線程有意義,兩個線程中每一個線程的執行時間必須至可能是單線程代碼的一半。若是把單線程和雙線程放到等式的兩遍,那麼惟一的變量就是cache命中率。不使線程執行速度下降50%或更多(下降超過50%就比單線程慢了),而後計算所需的最小cache命中率,獲得下面這張圖:
Figure 3.23: Minimum Cache Hit Rate For Speed-Up
X軸表明了單線程代碼的Ghit,Y表明了雙線程代碼所需的Ghit,雙線程的值永遠不能比單線程高,不然的話就意味着單線程能夠用一樣的方法改進代碼了。單線程Ghit < 55%的時候,程序老是可以從多線程獲得好處。
綠色表明的是目標區域,若是一個線程的降速低於50%且每一個線程的工做量減半,那麼運行時間是有可能低於單線程的運行時間的。看上圖,單線程Ghit=60%時,若是要獲得好處,雙線程程序必須在10%以上。若是單線程Ghit=95%,多線程則必須在80%以上,這就難了。特別地,這個問題是關於hyper-threads自己的,實際上給每一個hyper-thread的cache尺寸是減半的(L1d、L二、L3都是)。兩個hyper-thread使用相同的cache來加載數據。若是兩個線程的工做集不重疊,那麼原95%也可能減半,那麼就遠低於要求的80%。
因此Hyper-threads只在有限範圍的場景下有用。單線程下的cache命中率必須足夠低,並且就算減半cache大小新的cache命中率在等式中依然可以達到目標。也只有這樣使用Hyper-thread纔有意義。在實踐中是否可以更快取決於處理器是否可以充分交疊一個線程的等待和另外一個線程的執行。而代碼爲了並行處理所引入的其餘開銷也是要考慮進去的。
因此很明白的就是,若是兩個hyper-threads運行的是兩個徹底不一樣的代碼,那麼確定不會帶來什麼好處的,除非cache足夠大到可以抵消因cache減半致使的cache miss率的提升。除非操做系統的工做負載由一堆在設計上真正可以從hyper-thread獲益的進程組成,可能仍是在BIOS裏關掉hyper-thread比較好。
現代處理器提供給進程的虛擬地址空間(virtual address space),也就是說有兩種地址:虛擬的和物理的。
虛擬地址不是惟一的:
處理器使用虛擬地址,虛擬地址必須在Memory Management Unit(MMU)的幫助下才能翻譯成物理地址。不過這個步驟是很耗時的(注:前面提到的TLB cache緩存的是虛擬->物理地址的翻譯結果)。
現代處理器被設計成爲L1d、L1i使用虛擬地址,更高層的cache則使用物理地址。
一般來講沒必要關心cache地址處理的細節,由於這些是不能改變的。
Overflowing the cache capacity是一件壞事情;若是大多數使用的cache line都屬於同一個set,則全部緩存都會提早遇到問題。第二個問題能夠經過virtual address來解決,可是沒法避免user-level進程使用物理地址來緩存。惟一須要記住的事情是,要盡一切可能,不要在同一個進程裏把相同的物理地址位置映射到兩個或更多虛擬地址。
Cache replacement策略,目前使用的是LRU策略。關於cache replacement程序員沒有什麼事情可作。程序員能作的事情是:
指令cache比數據cache問題更少,緣由是:
CPU核心和cache(甚至第一級cache)的速度差別在增長。CPU已被流水線化,所謂流水線指一條指令的執行是分階段的。首先指令被解碼,參數被準備,最後被執行。有時候這個pipeline會很長,長pipeline就意味着若是pipeline中止(好比一條指令的執行被中斷了),那它得花一些時間才能從新找回速度。pipeline中止老是會發生的,好比沒法正確預測下一條指令,或者花太長時間加載下一條指令(好比從內存里加載)。
現代CPU設計師花費了大量時間和芯片資產在分支預測上,爲了儘量不頻繁的發生pipeline中止。
早些時候爲了下降內存使用(內存那個時候很貴),人們使用一種叫作Self Modifing Code(SMC)的技術來減小程序數據的尺寸。
不過如今應該避免SMC,由於若是處理器加載一條指令到流水線中,而這條指令在卻又被修改了,那麼整個工做就要從頭來過。
因此如今處處理器假設code pages是不可變的,因此L1i沒有使用MESI,而是使用更簡單的SI
咱們已經看到內存訪問cache miss致使的開銷極具增大,可是有時候這個問題是沒法避免的,因此理解實際的開銷以及如何緩解這個問題是很重要的。
測試程序使用x86和x86-64處理器的SSE指令每次加載16 bytes,working set size從1K到512M,測試的是每一個週期可以加載/存儲多少個bytes。
Figure 3.24: Pentium 4 Bandwidth
這個圖是64-bit Intel Netburst處理器的測試結果。
先看讀的測試能夠看到:
在看寫和copy的測試:
下面是兩個線程分別釘在同一核心的兩個Hyper-thread上的測試狀況:
Figure 3.25: P4 Bandwidth with 2 Hyper-Threads
這個結果符合預期,由於Hyper-thread除了不共享register以外,其餘資源都是共享的,cache和帶寬都被減半了。這意味着就算一個線程在在等待內存的時候可讓另外一個線程執行,可是另外一個線程實際上也在等待,因此並無什麼區別。
下圖是Intel Core2處理器的測試結果(對比Figure 3.24):
Figure 3.26: Core 2 Bandwidth
Core 2 L2是P4 L2的四倍。這延遲了write和copy測試的性能下跌。read測試在全部的working set size裏都維持在16 bytes/cycle左右,在220處下跌了一點點是由於DTLB放不下working set。可以有這麼好的性能表現不只意味着處理器可以及時地prefetching和傳輸數據,也意味着數據是被prefetch到L1d的。
看write和copy的表現,Core 2處理器的L1d cache沒有采用Write-through策略,只有當必要的時候L1d纔會evict,因此可以得到和read接近的表現。當L1d不夠用的時候,性能開始降低。當L2不夠用的時候再下跌不少。
下圖是Intel Core2處理器雙線程跑在兩個核心上:
Figure 3.27: Core 2 Bandwidth with 2 Threads
這個測試兩個線程分別跑在兩個核心上,兩個線程訪問相同的內存,沒有完美地同步。read性能和Figure 3.26沒有什麼差異。
看write和copy的表現,有意思的是在working set可以放進L1d的表現和直接從main memory讀取的表現同樣。兩個線程訪問相同的內存區域,針對cache line的RFO消息確定會被髮出。這裏能夠看到一個問題,RFO的處理速度沒有L2快。當L1d不夠用的時候,會將修改的cache line flush到共享的L2,這是性能有顯著提高是由於L1d中的數據被flush到L2,那就沒有RFO了。並且由於兩個核心共享FSB,每一個核心只擁有一半的FSB帶寬,意味着每一個線程的性能大約是單線程的一半。
廠商的不一樣版本CPU和不一樣廠商的CPU的表現都是不一樣的。原文裏後面比較了AMD Opteron處理器,這裏就不寫了。
數據是一block爲單位從main memory傳輸到cache line的,一個block大小爲64 bits(記得前面說的word也是64 bits),一條cache line的大小是64/128 bytes,因此填滿一個cache line要傳輸8/16次。
後面沒有看懂,大體意思是Critical word是cache line中的關鍵word,程序要讀到它以後才能繼續運行,可是若是critical word不是cache line的第一個,那麼就得等前面的word都加載完了以後才行。blah blah blah,不過這個理解可能也是不對的。
cache和hyper-thread、core、處理器的關係是程序員沒法控制的,可是程序員能夠決定線程在哪裏執行,因此cache和CPU是如何關聯的就顯得重要了。
後面沒仔細看,大體講了因爲不一樣的CPU架構,決定如何調度線程是比較複雜的。
不細講了,對比了667MHz DDR2和800 MHz DDR2(20%的提高),測試下來性能有18%的提高接近理論值(20%)。
Figure 3.32: Influence of FSB Speed
因此更快的FSB的確可以帶來好處。要注意CPU可能支持更高的FSB,可是主板/北橋可能不支持的狀況。