1. 程式人生 > >每個程式設計師都應該瞭解的 CPU 快取記憶體 英文原文:Memory part 2: CPU caches

每個程式設計師都應該瞭解的 CPU 快取記憶體 英文原文:Memory part 2: CPU caches

現在的CPU比25年前要精密得多了。在那個年代,CPU的頻率與記憶體匯流排的頻率基本在同一層面上。記憶體的訪問速度僅比暫存器慢那麼一點點。但是,這一局面在上世紀90年代被打破了。CPU的頻率大大提升,但記憶體匯流排的頻率與記憶體晶片的效能卻沒有得到成比例的提升。並不是因為造不出更快的記憶體,只是因為太貴了。記憶體如果要達到目前CPU那樣的速度,那麼它的造價恐怕要貴上好幾個數量級。

如果有兩個選項讓你選擇,一個是速度非常快、但容量很小的記憶體,一個是速度還算快、但容量很多的記憶體,如果你的工作集比較大,超過了前一種情況,那麼人們總是會選擇第二個選項。原因在於輔存(一般為磁碟)的速度。由於工作集超過主存,那麼必須用輔存來儲存交換出去的那部分資料,而輔存的速度往往要比主存慢上好幾個數量級。

好在這問題也並不全然是非甲即乙的選擇。在配置大量DRAM的同時,我們還可以配置少量SRAM。將地址空間的某個部分劃給SRAM,剩下的部分劃給DRAM。一般來說,SRAM可以當作擴充套件的暫存器來使用。

上面的做法看起來似乎可以,但實際上並不可行。首先,將SRAM記憶體對映到程序的虛擬地址空間就是個非常複雜的工作,而且,在這種做法中,每個程序都需要管理這個SRAM區記憶體的分配。每個程序可能有大小完全不同的SRAM區,而組成程式的每個模組也需要索取屬於自身的SRAM,更引入了額外的同步需求。簡而言之,快速記憶體帶來的好處完全被額外的管理開銷給抵消了。

因此,SRAM是作為CPU自動使用和管理的一個資源,而不是由OS或者使用者管理的。在這種模式下,SRAM用來複制儲存(或者叫快取)主記憶體中有可能即將被CPU使用的資料。這意味著,在較短時間內,CPU很有可能重複執行某一段程式碼,或者重複使用某部分資料。從程式碼上看,這意味著CPU執行了一個迴圈,所以相同的程式碼一次又一次地執行(空間區域性性的絕佳例子)。資料訪問也相對侷限在一個小的區間內。即使程式使用的實體記憶體不是相連的,在短期內程式仍然很有可能使用同樣的資料(時間區域性性)。這個在程式碼上表現為,程式在一個迴圈體內呼叫了入口一個位於另外的實體地址的函式。這個函式可能與當前指令的物理位置相距甚遠,但是呼叫的時間差不大。在資料上表現為,程式使用的記憶體是有限的(相當於工作集的大小)。但是實際上由於RAM的隨機訪問特性,程式使用的實體記憶體並不是連續的。正是由於空間區域性性和時間區域性性的存在,我們才提煉出今天的CPU快取概念。


我們先用一個簡單的計算來展示一下快取記憶體的效率。假設,訪問主存需要200個週期,而訪問快取記憶體需要15個週期。如果使用100個數據元素100次,那麼在沒有快取記憶體的情況下,需要2000000個週期,而在有快取記憶體、而且所有資料都已被快取的情況下,只需要168500個週期。節約了91.5%的時間。

用作快取記憶體的SRAM容量比主存小得多。以我的經驗來說,快取記憶體的大小一般是主存的千分之一左右(目前一般是4GB主存、4MB快取)。這一點本身並不是什麼問題。只是,計算機一般都會有比較大的主存,因此工作集的大小總是會大於快取。特別是那些執行多程序的系統,它的工作集大小是所有程序加上核心的總和。

處理快取記憶體大小的限制需要制定一套很好的策略來決定在給定的時間內什麼資料應該被快取。由於不是所有資料的工作集都是在完全相同的時間段內被使用的,我們可以用一些技術手段將需要用到的資料臨時替換那些當前並未使用的快取資料。這種預取將會減少部分訪問主存的成本,因為它與程式的執行是非同步的。所有的這些技術將會使快取記憶體在使用的時候看起來比實際更大。我們將在3.3節討論這些問題。 
我們將在第6章討論如何讓這些技術能很好地幫助程式設計師,讓處理器更高效地工作。

3.1 快取記憶體的位置

在深入介紹快取記憶體的技術細節之前,有必要說明一下它在現代計算機系統中所處的位置。

 
圖3.1: 最簡單的快取記憶體配置圖

圖3.1展示了最簡單的快取記憶體配置。早期的一些系統就是類似的架構。在這種架構中,CPU核心不再直連到主存。{在一些更早的系統中,快取記憶體像CPU與主存一樣連到系統總線上。那種做法更像是一種hack,而不是真正的解決方案。}資料的讀取和儲存都經過快取記憶體。CPU核心與快取記憶體之間是一條特殊的快速通道。在簡化的表示法中,主存與快取記憶體都連到系統總線上,這條匯流排同時還用於與其它元件通訊。我們管這條匯流排叫“FSB”——就是現在稱呼它的術語,參見第2.2節。在這一節裡,我們將忽略北橋。

在過去的幾十年,經驗表明使用了馮諾伊曼結構的 計算機,將用於程式碼和資料的快取記憶體分開是存在巨大優勢的。自1993年以來,Intel 並且一直堅持使用獨立的程式碼和資料快取記憶體。由於所需的程式碼和資料的記憶體區域是幾乎相互獨立的,這就是為什麼獨立快取工作得更完美的原因。近年來,獨立快取的另一個優勢慢慢顯現出來:常見處理器解碼 指令步驟 是緩慢的,尤其當管線為空的時候,往往會伴隨著錯誤的預測或無法預測的分支的出現, 將快取記憶體技術用於指令解碼可以加快其執行速度。

在快取記憶體出現後不久,系統變得更加複雜。快取記憶體與主存之間的速度差異進一步拉大,直到加入了另一級快取。新加入的這一級快取比第一級快取更大,但是更慢。由於加大一級快取的做法從經濟上考慮是行不通的,所以有了二級快取,甚至現在的有些系統擁有三級快取,如圖3.2所示。隨著單個CPU中核數的增加,未來甚至可能會出現更多層級的快取。

 
圖3.2: 三級快取的處理器

圖3.2展示了三級快取,並介紹了本文將使用的一些術語。L1d是一級資料快取,L1i是一級指令快取,等等。請注意,這只是示意圖,真正的資料流並不需要流經上級快取。CPU的設計者們在設計快取記憶體的介面時擁有很大的自由。而程式設計師是看不到這些設計選項的。

另外,我們有多核CPU,每個核心可以有多個“執行緒”。核心與執行緒的不同之處在於,核心擁有獨立的硬體資源({早期的多核CPU甚至有獨立的二級快取。})。在不同時使用相同資源(比如,通往外界的連線)的情況下,核心可以完全獨立地執行。而執行緒只是共享資源。Intel的執行緒只有獨立的暫存器,而且還有限制——不是所有暫存器都獨立,有些是共享的。綜上,現代CPU的結構就像圖3.3所示。

 
圖3.3 多處理器、多核心、多執行緒

在上圖中,有兩個處理器,每個處理器有兩個核心,每個核心有兩個執行緒。執行緒們共享一級快取。核心(以深灰色表示)有獨立的一級快取,同時共享二級快取。處理器(淡灰色)之間不共享任何快取。這些資訊很重要,特別是在討論多程序和多執行緒情況下快取的影響時尤為重要。

3.2 高階的快取操作

瞭解成本和節約使用快取,我們必須結合在第二節中講到的關於計算機體系結構和RAM技術,以及前一節講到的快取描述來探討。

預設情況下,CPU核心所有的資料的讀或寫都儲存在快取中。當然,也有記憶體區域不能被快取的,但是這種情況只發生在作業系統的實現者對資料考慮的前提下;對程式實現者來說,這是不可見的。這也說明,程式設計者可以故意繞過某些快取,不過這將是第六節中討論的內容了。

如果CPU需要訪問某個字(word),先檢索快取。很顯然,快取不可能容納主存所有內容(否則還需要主存幹嘛)。系統用字的記憶體地址來對快取條目進行標記。如果需要讀寫某個地址的字,那麼根據標籤來檢索快取即可。這裡用到的地址可以是虛擬地址,也可以是實體地址,取決於快取的具體實現。

標籤是需要額外空間的,用字作為快取的粒度顯然毫無效率。比如,在x86機器上,32位字的標籤可能需要32位,甚至更長。另一方面,由於空間區域性性的存在,與當前地址相鄰的地址有很大可能會被一起訪問。再回憶下2.2.1節——記憶體模組在傳輸位於同一行上的多份資料時,由於不需要傳送新CAS訊號,甚至不需要傳送RAS訊號,因此可以實現很高的效率。基於以上的原因,快取條目並不儲存單個字,而是儲存若干連續字組成的“線”。在早期的快取中,線長是32位元組,現在一般是64位元組。對於64位寬的記憶體匯流排,每條線需要8次傳輸。而DDR對於這種傳輸模式的支援更為高效。

當處理器需要記憶體中的某塊資料時,整條快取線被裝入L1d。快取線的地址通過對記憶體地址進行掩碼操作生成。對於64位元組的快取線,是將低6位置0。這些被丟棄的位作為線內偏移量。其它的位作為標籤,並用於在快取內定位。在實踐中,我們將地址分為三個部分。32位地址的情況如下:

如果快取線長度為2O,那麼地址的低O位用作線內偏移量。上面的S位選擇“快取集”。後面我們會說明使用快取集的原因。現在只需要明白一共有2S個快取集就夠了。剩下的32 - S - O = T位組成標籤。它們用來區分別名相同的各條線{有相同S部分的快取線被稱為有相同的別名。}用於定位快取集的S部分不需要儲存,因為屬於同一快取集的所有線的S部分都是相同的。

當某條指令修改記憶體時,仍然要先裝入快取線,因為任何指令都不可能同時修改整條線(只有一個例外——第6.1節中將會介紹的寫合併(write-combine))。因此需要在寫操作前先把快取線裝載進來。如果快取線被寫入,但還沒有寫回主存,那就是所謂的“髒了”。髒了的線一旦寫回主存,髒標記即被清除。

為了裝入新資料,基本上總是要先在快取中清理出位置。L1d將內容逐出L1d,推入L2(線長相同)。當然,L2也需要清理位置。於是L2將內容推入L3,最後L3將它推入主存。這種逐出操作一級比一級昂貴。這裡所說的是現代AMD和VIA處理器所採用的獨佔型快取(exclusive cache)。而Intel採用的是包容型快取(inclusive cache),{並不完全正確,Intel有些快取是獨佔型的,還有一些快取具有獨佔型快取的特點。}L1d的每條線同時存在於L2裡。對這種快取,逐出操作就很快了。如果有足夠L2,對於相同內容存在不同地方造成記憶體浪費的缺點可以降到最低,而且在逐出時非常有利。而獨佔型快取在裝載新資料時只需要操作L1d,不需要碰L2,因此會比較快。

處理器體系結構中定義的作為儲存器的模型只要還沒有改變,那就允許多CPU按照自己的方式來管理快取記憶體。這表示,例如,設計優良的處理器,利用很少或根本沒有記憶體匯流排活動,並主動寫回主記憶體髒快取記憶體行。這種快取記憶體架構在如x86和x86-64各種各樣的處理器間存在。製造商之間,即使在同一製造商生產的產品中,證明了的記憶體模型抽象的力量。

在對稱多處理器(SMP)架構的系統中,CPU的快取記憶體不能獨立的工作。在任何時候,所有的處理器都應該擁有相同的記憶體內容。保證這樣的統一的記憶體檢視被稱為“快取記憶體一致性”。如果在其自己的快取記憶體和主記憶體間,處理器設計簡單,它將不會看到在其他處理器上的髒快取記憶體行的內容。從一個處理器直接訪問另一個處理器的快取記憶體這種模型設計代價將是非常昂貴的,它是一個相當大的瓶頸。相反,當另一個處理器要讀取或寫入到快取記憶體線上時,處理器會去檢測。 

如果CPU檢測到一個寫訪問,而且該CPU的cache中已經快取了一個cache line的原始副本,那麼這個cache line將被標記為無效的cache line。接下來在引用這個cache line之前,需要重新載入該cache line。需要注意的是讀訪問並不會導致cache line被標記為無效的。

更精確的cache實現需要考慮到其他更多的可能性,比如第二個CPU在讀或者寫他的cache line時,發現該cache line在第一個CPU的cache中被標記為髒資料了,此時我們就需要做進一步的處理。在這種情況下,主儲存器已經失效,第二個CPU需要讀取第一個CPU的cache line。通過測試,我們知道在這種情況下第一個CPU會將自己的cache line資料自動傳送給第二個CPU。這種操作是繞過主儲存器的,但是有時候儲存控制器是可以直接將第一個CPU中的cache line資料儲存到主儲存器中。對第一個CPU的cache的寫訪問會導致本地cache line的所有拷貝被標記為無效。

隨著時間的推移,一大批快取一致性協議已經建立。其中,最重要的是MESI,我們將在第3.3.4節進行介紹。以上結論可以概括為幾個簡單的規則: 
  • 一個髒快取線不存在於任何其他處理器的快取之中。
  • 同一快取線中的乾淨拷貝可以駐留在任意多個其他快取之中。
如果遵守這些規則,處理器甚至可以在多處理器系統中更加有效的使用它們的快取。所有的處理器需要做的就是監控其他每一個寫訪問和比較本地快取中的地址。在下一節中,我們將介紹更多細節方面的實現,尤其是儲存開銷方面的細節。 

最後,我們至少應該關注快取記憶體命中或未命中帶來的消耗。下面是英特爾奔騰 M 的資料:

To Where Cycles
Register <= 1
L1d ~3
L2 ~14
Main Memory ~240

這是在CPU週期中的實際訪問時間。有趣的是,對於L2快取記憶體的訪問時間很大一部分(甚至是大部分)是由線路的延遲引起的。這是一個限制,增加快取記憶體的大小變得更糟。只有當減小時(例如,從60納米的Merom到45納米Penryn處理器),可以提高這些資料。

表格中的數字看起來很高,但是,幸運的是,整個成本不必須負擔每次出現的快取載入和快取失效。某些部分的成本可以被隱藏。現在的處理器都使用不同長度的內部管道,在管道內指令被解碼,併為準備執行。如果資料要傳送到一個暫存器,那麼部分的準備工作是從儲存器(或快取記憶體)載入資料。如果記憶體載入操作在管道中足夠早的進行,它可以與其他操作並行發生,那麼載入的全部發銷可能會被隱藏。對L1D常常可能如此;某些有長管道的處理器的L2也可以。 

提早啟動記憶體的讀取有許多障礙。它可能只是簡單的不具有足夠資源供記憶體訪問,或者地址從另一個指令獲取,然後載入的最終地址才變得可用。在這種情況下,載入成本是不能隱藏的(完全的)。 

對於寫操作,CPU並不需要等待資料被安全地放入記憶體。只要指令具有類似的效果,就沒有什麼東西可以阻止CPU走捷徑了。它可以早早地執行下一條指令,甚至可以在影子暫存器(shadow register)的幫助下,更改這個寫操作將要儲存的資料。

 
圖3.4: 隨機寫操作的訪問時間

圖3.4展示了快取的效果。關於產生圖中資料的程式,我們會在稍後討論。這裡大致說下,這個程式是連續隨機地訪問某塊大小可配的記憶體區域。每個資料項的大小是固定的。資料項的多少取決於選擇的工作集大小。Y軸表示處理每個元素平均需要多少個CPU週期,注意它是對數刻度。X軸也是同樣,工作集的大小都以2的n次方表示。

圖中有三個比較明顯的不同階段。很正常,這個處理器有L1d和L2,沒有L3。根據經驗可以推測出,L1d有213位元組,而L2有220位元組。因為,如果整個工作集都可以放入L1d,那麼只需不到10個週期就可以完成操作。如果工作集超過L1d,處理器不得不從L2獲取資料,於是時間飄升到28個週期左右。如果工作集更大,超過了L2,那麼時間進一步暴漲到480個週期以上。這時候,許多操作將不得不從主存中獲取資料。更糟糕的是,如果修改了資料,還需要將這些髒了的快取線寫回記憶體。

看了這個圖,大家應該會有足夠的動力去檢查程式碼、改進快取的利用方式了吧?這裡的效能改善可不只是微不足道的幾個百分點,而是幾個數量級呀。在第6節中,我們將介紹一些編寫高效程式碼的技巧。而下一節將進一步深入快取的設計。雖然精彩,但並不是必修課,大家可以選擇性地跳過。

3.3 CPU快取實現的細節

快取的實現者們都要面對一個問題——主存中每一個單元都可能需被快取。如果程式的工作集很大,就會有許多記憶體位置為了快取而打架。前面我們曾經提過快取與主存的容量比,1:1000也十分常見。

3.3.1 關聯性

我們可以讓快取的每條線能存放任何記憶體地址的資料。這就是所謂的全關聯快取(fully associative cache)。對於這種快取,處理器為了訪問某條線,將不得不檢索所有線的標籤。而標籤則包含了整個地址,而不僅僅只是線內偏移量(也就意味著,圖3.2中的S為0)。

快取記憶體有類似這樣的實現,但是,看看在今天使用的L2的數目,表明這是不切實際的。給定4MB的快取記憶體和64B的快取記憶體段,快取記憶體將有65,536個項。為了達到足夠的效能,快取邏輯必須能夠在短短的幾個時鐘週期內,從所有這些項中,挑一個匹配給定的標籤。實現這一點的工作將是巨大的。

Figure 3.5: 全關聯快取記憶體原理圖

對於每個快取記憶體行,比較器是需要比較大標籤(注意,S是零)。每個連線旁邊的字母表示位的寬度。如果沒有給出,它是一個單位元線。每個比較器都要比較兩個T-位寬的值。然後,基於該結果,適當的快取記憶體行的內容被選中,並使其可用。這需要合併多套O資料線,因為他們是快取桶(譯註:這裡類似把O輸出接入多選器,所以需要合併)。實現僅僅一個比較器,需要電晶體的數量就非常大,特別是因為它必須非常快。沒有迭代比較器是可用的。節省比較器的數目的唯一途徑是通過反覆比較標籤,以減少它們的數目。這是不適合的,出於同樣的原因,迭代比較器不可用:它的時間太長。

全關聯快取記憶體對 小快取是實用的(例如,在某些Intel處理器的TLB快取是全關聯的),但這些快取都很小,非常小的。我們正在談論的最多幾十項。 

對於L1i,L1d和更高級別的快取,需要採用不同的方法。可以做的就是是限制搜尋。最極端的限制是,每個標籤對映到一個明確的快取條目。計算很簡單:給定的4MB/64B快取有65536項,我們可以使用地址的bit6到bit21(16位)來直接定址快取記憶體的每一個項。地址的低6位作為快取記憶體段的索引。 

Figure 3.6: Direct-Mapped Cache Schematics

在圖3.6中可以看出,這種直接對映的快取記憶體,速度快,比較容易實現。它只是需要一個比較器,一個多路複用器(在這個圖中有兩個,標記和資料是分離的,但是對於設計這不是一個硬性要求),和一些邏輯來選擇只是有效的快取記憶體行的內容。由於速度的要求,比較器是複雜的,但是現在只需要一個,結果是可以花更多的精力,讓其變得快速。這種方法的複雜性在於在多路複用器。一個簡單的多路轉換器中的電晶體的數量增速是O(log N)的,其中N是快取記憶體段的數目。這是可以容忍的,但可能會很慢,在某種情況下,速度可提升,通過增加多路複用器電晶體數量,來並行化的一些工作和自身增速。電晶體的總數只是隨著快速增長的快取記憶體緩慢的增加,這使得這種解決方案非常有吸引力。但它有一個缺點:只有用於直接對映地址的相關的地址位均勻分佈,程式才能很好工作。如果分佈的不均勻,而且這是常態,一些快取項頻繁的使用,並因此多次被換出,而另一些則幾乎不被使用或一直是空的。

Figure 3.7: 組關聯快取記憶體原理圖

可以通過使快取記憶體的組關聯來解決此問題。組關聯結合快取記憶體的全關聯和直接對映快取記憶體特點,在很大程度上避免那些設計的弱點。圖3.7顯示了一個組關聯快取記憶體的設計。標籤和資料儲存分成不同的組並可以通過地址選擇。這類似直接對映快取記憶體。但是,小數目的值可以在同一個快取記憶體組快取,而不是一個快取組只有一個元素,用於在快取記憶體中的每個設定值是相同的一組值的快取。所有組的成員的標籤可以並行比較,這類似全關聯快取的功能。

其結果是快取記憶體,不容易被不幸或故意選擇同屬同一組編號的地址所擊敗,同時快取記憶體的大小並不限於由比較器的數目,可以以並行的方式實現。如果快取記憶體增長,只(在該圖中)增加列的數目,而不增加行數。只有快取記憶體之間的關聯性增加,行數才會增加。今天,處理器的L2快取記憶體或更高的快取記憶體,使用的關聯性高達16。 L1快取記憶體通常使用8。

L2
Cache
Size
Associativity
Direct 2 4 8
CL=32 CL=64 CL=32 CL=64 CL=32 CL=64 CL=32 CL=64
512k 27,794,595 20,422,527 25,222,611 18,303,581 24,096,510 17,356,121 23,666,929 17,029,334
1M 19,007,315 13,903,854 16,566,738 12,127,174 15,537,500 11,436,705 15,162,895 11,233,896
2M 12,230,962 8,801,403 9,081,881 6,491,011 7,878,601 5,675,181 7,391,389 5,382,064
4M 7,749,986 5,427,836 4,736,187 3,159,507 3,788,122 2,418,898 3,430,713 2,125,103
8M 4,731,904 3,209,693 2,690,498 1,602,957 2,207,655 1,228,190 2,111,075 1,155,847
16M 2,620,587 1,528,592 1,958,293 1,089,580 1,704,878 883,530 1,671,541 862,324

Table 3.1: 快取記憶體大小,關聯行,段大小的影響

給定我們4MB/64B快取記憶體,8路組關聯,相關的快取留給我們的有8192組,只用標籤的13位,就可以定址緩集。要確定哪些(如果有的話)的快取組設定中的條目包含定址的快取記憶體行,8個標籤都要進行比較。在很短的時間內做出來是可行的。通過一個實驗,我們可以看到,這是有意義的。

表3.1顯示一個程式在改變快取大小,快取段大小和關聯集大小,L2快取記憶體的快取失效數量(根據Linux核心相關的方面人的說法,GCC在這種情況下,是他們所有中最重要的標尺)。在7.2節中,我們將介紹工具來模擬此測試要求的快取記憶體。

 萬一這還不是很明顯,所有這些值之間的關係是快取記憶體的大小為:

cache line size × associativity × number of sets 

地址被對映到快取記憶體使用

O = log 2 cache line size 
S = log 2 number of sets

在第3.2節中的圖顯示的方式。

Figure 3.8: 快取段大小 vs 關聯行 (CL=32)

圖3.8表中的資料更易於理解。它顯示一個固定的32個位元組大小的快取記憶體行的資料。對於一個給定的快取記憶體大小,我們可以看出,關聯性,的確可以幫助明顯減少快取記憶體未命中的數量。對於8MB的快取,從直接對映到2路組相聯,可以減少近44%的快取記憶體未命中。組相聯快取記憶體和直接對映快取相比,該處理器可以把更多的工作集保持在快取中。

在文獻中,偶爾可以讀到,引入關聯性,和加倍快取記憶體的大小具有相同的效果。在從4M快取躍升到8MB快取的極端的情況下,這是正確的。關聯性再提高一倍那就肯定不正確啦。正如我們所看到的資料,後面的收益要小得多。我們不應該完全低估它的效果,雖然。在示例程式中的記憶體使用的峰值是5.6M。因此,具有8MB快取不太可能有很多(兩個以上)使用相同的快取記憶體的組。從較小的快取的關聯性的巨大收益可以看出,較大工作集可以節省更多

在一般情況下,增加8以上的快取記憶體之間的關聯性似乎對只有一個單執行緒工作量影響不大。隨著介紹一個使用共享L2的多核處理器,形勢發生了變化。現在你基本上有兩個程式命中相同的快取, 實際上導致快取記憶體減半(對於四核處理器是1/4)。因此,可以預期,隨著核的數目的增加,共享快取記憶體的相關性也應增長。一旦這種方法不再可行(16 路組關聯性已經很難)處理器設計者不得不開始使用共享的三級快取記憶體和更高級別的,而L2快取記憶體只被核的一個子集共享。

從圖3.8中,我們還可以研究快取大小對效能的影響。這一資料需要了解工作集的大小才能進行解讀。很顯然,與主存相同的快取比小快取能產生更好的結果,因此,快取通常是越大越好。

上文已經說過,示例中最大的工作集為5.6M。它並沒有給出最佳快取大小值,但我們可以估算出來。問題主要在於記憶體的使用並不連續,因此,即使是16M的快取,在處理5.6M的工作集時也會出現衝突(參見2路集合關聯式16MB快取vs直接對映式快取的優點)。不管怎樣,我們可以有把握地說,在同樣5.6M的負載下,快取從16MB升到32MB基本已沒有多少提高的餘地。但是,工作集是會變的。如果工作集不斷增大,快取也需要隨之增大。在購買計算機時,如果需要選擇快取大小,一定要先衡量工作集的大小。原因可以參見圖3.10。

 
圖3.9: 測試的記憶體分佈情況

我們執行兩項測試。第一項測試是按順序地訪問所有元素。測試程式循著指標n進行訪問,而所有元素是連結在一起的,從而使它們的被訪問順序與在記憶體中排布的順序一致,如圖3.9的下半部分所示,末尾的元素有一個指向首元素的引用。而第二項測試(見圖3.9的上半部分)則是按隨機順序訪問所有元素。在上述兩個測試中,所有元素都構成一個單向迴圈連結串列。

3.3.2 Cache的效能測試

用於測試程式的資料可以模擬一個任意大小的工作集:包括讀、寫訪問,隨機、連續訪問。在圖3.4中我們可以看到,程式為工作集建立了一個與其大小和元素型別相同的陣列:

  struct l {
    struct l *n;
    long int pad[NPAD];
  };

n欄位將所有節點隨機得或者順序的加入到環形連結串列中,用指標從當前節點進入到下一個節點。pad欄位用來儲存資料,其可以是任意大小。在一些測試程式中,pad欄位是可以修改的, 在其他程式中,pad欄位只可以進行讀操作。

在效能測試中,我們談到工作集大小的問題,工作集使用結構體l定義的元素表示的。2N 位元組的工作集包含

N/sizeof(struct l)

個元素. 顯然sizeof(struct l) 的值取決於NPAD的大小。在32位系統上,NPAD=7意味著陣列的每個元素的大小為32位元組,在64位系統上,NPAD=7意味著陣列的每個元素的大小為64位元組。

單執行緒順序訪問

最簡單的情況就是遍歷連結串列中順序儲存的節點。無論是從前向後處理,還是從後向前,對於處理器來說沒有什麼區別。下面的測試中,我們需要得到處理連結串列中一個元素所需要的時間,以CPU時鐘週期最為計時單元。圖3.10顯示了測試結構。除非有特殊說明, 所有的測試都是在Pentium 4 64-bit 平臺上進行的,因此結構體l中NPAD=0,大小為8位元組。

圖 3.10: 順序讀訪問, NPAD=0

圖 3.11: 順序讀多個位元組

一開始的兩個測試資料收到了噪音的汙染。由於它們的工作負荷太小,無法過濾掉系統內其它程序對它們的影響。我們可以認為它們都是4個週期以內的。這樣一來,整個圖可以劃分為比較明顯的三個部分:

  • 工作集小於214位元組的。
  • 工作集從215位元組到220位元組的。
  • 工作集大於221位元組的。

這樣的結果很容易解釋——是因為處理器有16KB的L1d和1MB的L2。而在這三個部分之間,並沒有非常銳利的邊緣,這是因為系統的其它部分也在使用快取,我們的測試程式並不能獨佔快取的使用。尤其是L2,它是統一式的快取,處理器的指令也會使用它(注: Intel使用的是包容式快取)。

測試的實際耗時可能會出乎大家的意料。L1d的部分跟我們預想的差不多,在一臺P4上耗時為4個週期左右。但L2的結果則出乎意料。大家可能覺得需要14個週期以上,但實際只用了9個週期。這要歸功於處理器先進的處理邏輯,當它使用連續的記憶體區時,會 預先讀取下一條快取線的資料。這樣一來,當真正使用下一條線的時候,其實已經早已讀完一半了,於是真正的等待耗時會比L2的訪問時間少很多。

在工作集超過L2的大小之後,預取的效果更明顯了。前面我們說過,主存的訪問需要耗時200個週期以上。但在預取的幫助下,實際耗時保持在9個週期左右。200 vs 9,效果非常不錯。

我們可以觀察到預取的行為,至少可以間接地觀察到。圖3.11中有4條線,它們表示處理不同大小結構時的耗時情況。隨著結構的變大,元素間的距離變大了。圖中4條線對應的元素距離分別是0、56、120和248位元組。

圖中最下面的這一條線來自前一個圖,但在這裡更像是一條直線。其它三條線的耗時情況比較差。圖中這些線也有比較明顯的三個階段,同時,在小工作集的情況下也有比較大的錯誤(請再次忽略這些錯誤)。在只使用L1d的階段,這些線條基本重合。因為這時候還不需要預取,只需要訪問L1d就行。

在L2階段,三條新加的線基本重合,而且耗時比老的那條線高很多,大約在28個週期左右,差不多就是L2的訪問時間。這表明,從L2到L1d的預取並沒有生效。這是因為,對於最下面的線(NPAD=0),由於結構小,8次迴圈後才需要訪問一條新快取線,而上面三條線對應的結構比較大,拿相對最小的NPAD=7來說,光是一次迴圈就需要訪問一條新線,更不用說更大的NPAD=15和31了。而預取邏輯是無法在每個週期裝載新線的,因此每次迴圈都需要從L2讀取,我們看到的就是從L2讀取的時延。

更有趣的是工作集超過L2容量後的階段。快看,4條線遠遠地拉開了。元素的大小變成了主角,左右了效能。處理器應能識別每一步(stride)的大小,不去為NPAD=15和31獲取那些實際並不需要的快取線(參見6.3.1)。元素大小對預取的約束是根源於硬體預取的限制——它無法跨越頁邊界。如果允許預取器跨越頁邊界,而下一頁不存在或無效,那麼OS還得去尋找它。這意味著,程式需要遭遇一次並非由它自己產生的頁錯誤,這是完全不能接受的。在NPAD=7或者更大的時候,由於每個元素都至少需要一條快取線,預取器已經幫不上忙了,它沒有足夠的時間去從記憶體裝載資料。

相關推薦

每個程式設計師應該瞭解CPU 快取記憶體 英文原文Memory part 2: CPU caches

現在的CPU比25年前要精密得多了。在那個年代,CPU的頻率與記憶體匯流排的頻率基本在同一層面上。記憶體的訪問速度僅比暫存器慢那麼一點點。但是,這一局面在上世紀90年代被打破了。CPU的頻率大大提升,但記憶體匯流排的頻率與記憶體晶片的效能卻沒有得到成比例的提升。並不是因為

每個程式設計師應該瞭解記憶體知識(二)

http://web.itivy.com/article-347-1.html 接下來的章節會涉及更多的有關訪問DRAM儲存器的實際操作的細節。我們不會提到更多有關訪問SRAM的具體內容,它通常是直接定址。這裡是由於速度和有限的SRAM儲存器的尺寸。SRAM現在應用在

每個程式設計師應該瞭解記憶體知識1——記憶體概述

1、概述     早期的計算機很簡單,它的各種元件如CPU、記憶體、大容量儲存和網路介面都是一起開發的,所以效能差不多。舉個例子來說,記憶體和網路介面提供資料的速度不會比CPU快多少。     這種情況隨著計算機基本結構的固化和各子系統的優化慢慢地發生了改變。其中一些元件

每個程式設計師應該瞭解的十一句話

1.技術只是解決問題的選擇,而不是解決問題的根本 我們可以因為掌握了最新的JavaScript框架ahem、Angular的IoC容器技術或者某些程式語言甚至作業系統而歡欣雀躍,但是這些東西並不是作為程式設計師的我們用來解決問題的根本——它們只是用於幫助我們解決問題的簡單工具。 我們必須非常謹慎,不要對某項正

每個程式設計師應當瞭解的11句話

1.技術只是解決問題的選擇,而不是解決問題的根本 我們可以因為掌握了最新的JavaScript框架ahem、Angular的IoC容器技術或者某些程式語言甚至作業系統而歡欣雀躍,但是這些東西並不是作為程式設計師的我們用來解決問題的根本——它們只是用於幫助我們解決問題的簡單工具。 我們必須非常謹慎

[轉]國外程式設計師推薦每個程式設計師應該讀的非程式設計書

五年前有網友在 Stackoverflow 發帖提問:『程式設計師應該讀哪些非程式設計方面的書?』。有很多程式設計師響應,他們在推薦的同時也寫下了自己的評語。本文摘編其中 29 本書,下面就按照各書的推薦數排列。另外,本月初我們在伯樂頭條也發起了相同的討論帖《你最喜歡的非程式設計書是哪一本?》,已有很多的朋友

程式猿養生方法(每個程式設計師應該看一看)

前言 程式設計師職業生涯中,健康問題尤為突出。隨著時間的流逝,夢想可能漸漸暗淡,激情可能慢慢消退,但是,有一點卻很肯定,我們的身體大不如前,視力下降,慢性腸胃炎,頸椎病,失眠,神經衰弱,此類慢性疾病接踵而來。 身體是自己的,也是一輩子的事情,人的自我恢復能力並不是很強;所以我向來不建議為了事業,而犧牲身體。

國外程式設計師推薦每個程式設計師應該讀的非程式設計書

【伯樂線上導讀】:五年前有網友在 Stackoverflow 發帖提問:『程式設計師應該讀哪些非程式設計方面的書?』。有很多程式設計師響應,他們在推薦的同時也寫下了自己的評語。本文摘編其中 29 本書,下面就按照各書的推薦數排列。另外,本月初我們在伯樂頭條也發起了相

每個程式設計師應該具備的除錯能力。

首先,除錯是⼀個程式設計師最基本的技能,其重要性甚⾄超過學習⼀門語⾔。不會除錯的程式設計師就意味著他即使會⼀門語⾔,卻不能編制出任何好的軟體。 VC/VS除錯快捷鍵: F9 //設定斷點和取消斷點 F10 //開始除錯//單步執⾏ F11 //進⼊

每個程式設計師應該知道的 15 個最佳 PHP 庫

1. PChart PChart是一個令人印象深刻的PHP庫,可以以一種視覺化圖表的形式生成文字資料。資料可以展示為柱狀圖,餅狀圖,以及其他格式。使用SQL查詢可以幫助PHP指令碼建立令人驚歎的圖表和圖形。 2. PHP CAPTCHA PHP CAPTCHA是另一個偉

每個程式設計師應該學習使用Python或Ruby(選Python)

每個程式設計師都應該學習使用Python或Ruby 如果你是個學生,你應該會C,C++和Java。還會一些VB,或C#/.NET。多少你還可能開發過一些Web網頁,你知道一些HTML,CSS和JavaScript知識。總體上說,我們很難發現會有學生顯露出掌握超出這幾種語言範

十大程式語言之父——每個程式設計師應該記住!

Dennis Ritchie(丹尼斯•裡奇)被世人尊稱為“無形之王的C語言之父”、“偉大的UNIX之父”,開創了計算機網路技術的先河,為喬布斯等IT巨匠提供肩膀的巨人。1978年與布萊恩•科爾尼幹(BrianW Kernighan)一起出版了名著《C程式設計語言》,被翻譯為多種語言,是C語

為什麼每個程式設計師應該學習使用命令列

大學畢業以後我就成了一名JAVA程式設計師,在之後的很長一段時間裡,我每天上班的流程基本都是一樣的: 早上來到公司 -> 開啟電腦(啟動Windows) -> 開啟Eclipse(我是一名JAVA程式設計師) -> 寫一天程式碼(期間我通過IDE整合的Tomcat伺服器來除錯我的應用

每個程式設計師應該知道的8個Linux命令

cat     cat – 連線檔案,並輸出結果 sort     sort – 檔案裡的文字按行排序 grep     grep, egrep, fgrep – 打印出匹配條件的文字行 cut     cut – 刪除檔案中字元行上的某些區域 sed     se

每個程式設計師應該收藏的演算法複雜度速查表

這篇文章覆蓋了計算機科學裡面常見演算法的時間和空間的大複雜度。我之前在參加面試前,經常需要花費很多時間從網際網路上查詢各種搜尋和排序演算法的優劣,以便我在面試時不會被問住。最近這幾年,我面試了幾家矽谷的初創企業和一些更大一些的公司,如 Yahoo、eBay、LinkedIn

【轉】為什麼我認為每個程式設計師應該用Mac OS X?

原文:http://tiny4.org/blog/2010/02/why-programmers-should-use-mac-os-x/ 查爾斯·狄更斯老師的《雙城記》裡有句非常著名的話,我每次看到都心潮澎湃,所以看了無數次《雙城記》總是在那兩句話前後打轉。心說,開頭就這麼

linux 每個程式設計師應該知道的8個Linux命令

每個程式設計師,在職業生涯的某個時刻,總會發現自己需要知道一些Linux方面的知識。我並不是說你應該成為一個Linux專家,我的意思是,當面對linux命令列任務時,你應該能很熟練的完成。事實上,學會了下面8個命令,我基本上能完成任何需要完成的任務。 注意:下

每個程式設計師應該學習的5種程式語言

瞭解一種或者真正的編碼語言是很好的,但作為一個真正的多語言開發人員是如何實現真正的主要狀態。 我在某處讀到程式設計師應該每年學習一種新的程式語言(我認為它的程式碼完整,但不確定),但如果你不能這樣做,我建議你至少學習以下五種程式語言,以便在你的職業生涯中取得好成績。 。

每個程式設計師應該讀的書

1. 《程式碼大全》 史蒂夫·邁克康奈爾 推薦數:1684 code complete 程式碼大全 “優秀的程式設計實踐的百科全書,《程式碼大全》注重個人技術,其中所有東西加起來,就是我們本能所說的“編寫整潔的程式碼”。這本書有50頁在談論程式碼佈局。” —— Joel S

每個程式設計師應該知道的

http://projectmona.com/bits-of-brilliance-session-five/ 裡面內容很雜但很豐富,是UIUC教授Jeff Erickson在程式設計方面的個人收集(其他收集可以參見:http://projectmona.com/bits-