閱讀82 返回首頁    go 阿裏雲 go 技術社區[雲棲]


4個你未必知道的內存小知識

除了CPU,內存大概是最重要的計算資源了。基本成為分布式係統標配的緩存中間件、高性能的數據處理係統及當前流行的大數據平台,都離不開對計算機內存的深入理解與巧妙使用。本文將探索這個讓人感到熟悉又複雜的領域。
本文選自《架構解密:從分布式到微服務》。

  1. 複雜的CPU與單純的內存
  2. 多核CPU與內存共享的問題
  3. 著名的Cache偽共享問題
  4. 深入理解不一致性內存

1 複雜的CPU與單純的內存

  首先,我們澄清幾個容易讓人混淆的CPU術語。

  • Socket或者Processor:指一個物理CPU芯片,盒裝的或者散裝的,上麵有很多針腳,直接安裝在主板上。
  • Core:指Socket裏封裝的一個CPU核心,每個Core都是完全獨立的計算單元,我們平時說的4核心CPU,就是指一個Socket(Processor)裏封裝了4個Core。
  • HT超線程:目前Intel與AMD的Processor大多支持在一個Core裏並行執行兩個線程,此時在操作係統看來就相當於兩個邏輯CPU(Logical Processor),在大多數情況下,我們在程序裏提到CPU這個概念時,就是指一個Logical Processor。

  然後,我們先從第1個非常簡單的問題開始:CPU可以直接操作內存嗎?可能99%的程序員會不假思索地回答:“肯定的,不然程序怎麼跑。”如果理性地分析一下,你會發現這個回答有問題:CPU與內存條是獨立的兩個硬件,而且CPU上也沒有插槽和連線可以讓內存條掛上去,也就是說,CPU並不能直接訪問內存條,而是要通過主板上的其他硬件(接口)來間接訪問內存條。
  第2個問題:CPU的運算速度與內存條的訪問速度之間的差距究竟有多大?這個差距跟王健林“先掙它個一個億的”小目標和“普通人有車有房”的宏大目標之間的差距相比,是更大還是更小呢?答案是“差不多”。通常來說,CPU的運算速度與內存訪問速度之間的差距不過是100倍,假如有100萬元人民幣就可以有房(貸)有車(貸)了,那麼其100倍剛好是一億元人民幣。
  既然CPU的速度與內存的速度還是存在高達兩個數量級的巨大鴻溝,所以它們注定不能“幸福地在一起”,於是CPU的親密伴侶Cache閃亮登場。與來自DRAM家族的內存(Memory)出身不同,Cache來自SRAM家族。DRAM與SRAM最簡單的區別是後者特別快,容量特別小,電路結構非常複雜,造價特別高。
  造成Cache與內存之間巨大性能差距的主要原因是工作原理和結構不同,如下所述。

  • DRAM存儲一位數據隻需要一個電容加一個晶體管,SRAM則需要6個晶體管。由於DRAM的數據其實是保存在電容裏的,所以每次讀寫過程中的充放電環節也導致了DRAM讀寫數據有一個延遲的問題,這個延遲通常為十幾到幾十ns。
  • 內存可以看作一個二維數組,每個存儲單元都有其行地址和列地址。由於SRAM的容量很小,所以存儲單元的地址(行與列)比較短,可以一次性傳輸到SRAM中;而DRAM則需要分別傳送行與列的地址。
  • SRAM的頻率基本與CPU的頻率保持一致;而DRAM的頻率直到DDR4以後才開始接近CPU的頻率。

  Cache是被集成到CPU內部的一個存儲單元,一級Cache(L1 Cache)通常隻有32~64KB的容量,這個容量遠遠不能滿足CPU大量、高速存取的需求。此外,由於存儲性能的大幅提升往往伴隨著價格的同步飆升,所以出於對整體成本的控製,現實中往往采用金字塔形的多級Cache體係來實現最佳緩存效果,於是出現了二級Cache(L2 Cache)及三級Cache(L3 Cache),每一級Cache都犧牲了部分性能指標來換取更大的容量,目的是緩存更多的熱點數據。以Intel家族Intel Sandy Bridge架構的CPU為例,其L1 Cache容量為64KB,訪問速度為1ns左右;L2 Cache容量擴大4倍,達到256KB,訪問速度則降低到3ns左右;L3 Cache的容量則擴大512倍,達到32MB,訪問速度也下降到12ns左右,即使如此,也比訪問主存的100ns(40ns+65ns)快一個數量級。此外,L3 Cache是被一個Socket上的所有CPU Core共享的,其實最早的L3 Cache被應用在AMD發布的K6-III處理器上,當時的L3 Cache受限於製造工藝,並沒有被集成到CPU內部,而是集成在主板上。
  下麵給出了Intel Sandy Bridge CPU的架構圖,我們可以看出,CPU如果要訪問內存中的數據,則要經過L1、L2與L3這三道關卡後才能抵達目的地,這個過程並不是“皇上”(CPU)親自出馬,而是交由3個級別的貴妃(Cache)們層層轉發“聖旨”(內存指令),最終抵達“後宮”(內存)。
        圖片描述

2 多核CPU與內存共享的問題

  現在恐怕很難再找到單核心的CPU了,即使是我們的智能手機,也至少是雙核的了,那麼問題就來了:在多核CPU的情況下,如何共享內存?
  如果擅長多線程高級編程,那麼你肯定會毫不猶豫地給出以下偽代碼解決方案:

 synchronized(memory)
    {
             writeAddress(….)
    }

  如果真這麼簡單,那麼這個世界上就不會隻剩下兩家獨大的主流CPU製造商了,而且可憐的AMD一直被Intel“吊打”。
  多核心CPU共享內存的問題也被稱為Cache一致性問題,簡單地說,就是多個CPU核心所看到的Cache數據應該是一致的,在某個數據被某個CPU寫入自己的Cache(L1 Cache)以後,其他CPU都應該能看到相同的Cache數據;如果自己的Cache中有舊數據,則拋棄舊數據。考慮到每個CPU有自己內部獨占的Cache,所以這個問題與分布式Cache保持同步的問題是同一類問題。來自Intel的MESI協議是目前業界公認的Cache一致性問題的最佳方案,大多數SMP架構都采用了這一方案,雖然該協議是一個CPU內部的協議,但由於它對我們理解內存模型及解決分布式係統中的數據一致性問題有重要的參考價值,所以在這裏我們對它進行簡單介紹。
  首先,我們說說Cache Line,如果有印象的話,則你會發現I/O操作從來不以字節為單位,而是以“塊”為單位,這裏有兩個原因:首先,因為I/O操作比較慢,所以讀一個字節與一次讀連續N個字節所花費的時間基本相同;其次,數據訪問往往具有空間連續性的特征,即我們通常會訪問空間上連續的一些數據。舉個例子,訪問數組時通常會循環遍曆,比如查找某個值或者進行比較等,如果把數組中連續的幾個字節都讀到內存中,那麼CPU的處理速度會提升幾倍。對於CPU來說,由於Memory也是慢速的外部組件,所以針對Memory的讀寫也采用類似I/O塊的方式就不足為奇了。實際上,CPU Cache裏的最小存儲單元就是Cache Line,Intel CPU的一個Cache Line存儲64個字節,每一級Cache都被劃分為很多組Cache Line,典型的情況是4條Cache Line為一組,當Cache從Memory中加載數據時,一次加載一條Cache Line的數據。下圖給出了Cache的結構。
          圖片描述
  每個Cache Line的頭部有兩個Bit來表示自身的狀態,總共有4種狀態。

  • M(Modified):修改狀態,其他CPU上沒有數據的副本,並且在本CPU上被修改過,與存儲器中的數據不一致,最終必然會引發係統總線的寫指令,將Cache Line中的數據寫回到Memory中。
  • E(Exclusive):獨占狀態,表示當前Cache Line中包含的數據與Memory中的數據一致,此外,其他CPU上沒有數據的副本。
  • S(Shared):共享狀態,表示Cache Line中包含的數據與Memory中的數據一致,而且在當前CPU和至少在其他某個CPU中有副本。
  • I(Invalid):無效狀態,當前Cache Line中沒有有效數據或者該Cache Line數據已經失效,不能再用,當Cache要加載新數據時,優先選擇此狀態的Cache Line,此外,Cache Line的初始狀態也是I狀態。

  MESI協議是用Cache Line的上述4種狀態命名的,對Cache的讀寫操作引發了Cache Line的狀態變化,因而可以理解為一種狀態機模型。但MESI的複雜和獨特之處在於狀態有兩種視角:一種是當前讀寫操作(Local Read/Write)所在CPU看到的自身的Cache Line狀態及其他CPU上對應的Cache Line狀態;另一種是一個CPU上的Cache Line狀態的變遷會導致其他CPU上對應的Cache Line的狀態變遷。如下所示為MESI協議的狀態圖。
         圖片描述
  結合這個狀態圖,我們深入分析MESI協議的一些實現細節。
  (1)某個CPU(CPU A)發起本地讀請求(Local Read),比如讀取某個內存地址的變量,如果此時所有CPU的Cache中都沒加載此內存地址,即此內存地址對應的Cache Line為無效狀態(Invalid),則CPU A中的Cache會發起一個到Memory的內存Load指令,在相應的Cache Line中完成內存加載後,此Cache Line的狀態會被標記為Exclusive。接下來,如果其他CPU(CPU B)在總線上也發起對同一個內存地址的讀請求,則這個讀請求會被CPU A嗅探到(SNOOP),然後CPU A在內存總線上複製一份Cache Line作為應答,並將自身的Cache Line狀態改為Shared,同時CPU B收到來自總線的應答並保存到自己的Cache裏,也修改對應的Cache Line狀態為Shared。
  (2)某個CPU(CPU A)發起本地寫請求(Local Write),比如對某個內存地址的變量賦值,如果此時所有CPU的Cache中都沒加載此內存地址,即此內存地址對應的Cache Line為無效狀態(Invalid),則CPU A中的Cache Line保存了最新的內存變量值後,其狀態被修改為Modified。隨後,如果CPU B發起對同一個變量的讀操作(Remote Read),則CPU A在總線上嗅探到這個讀請求以後,先將Cache Line裏修改過的數據回寫(Write Back)到Memory中,然後在內存總線上複製一份Cache Line作為應答,最後將自身的Cache Line狀態修改為Shared,由此產生的結果是CPU A與CPU B裏對應的Cache Line狀態都為Shared。
  (3)以上麵第2條內容為基礎,CPU A發起本地寫請求並導致自身的Cache Line狀態變為Modified,如果此時CPU B發起同一個內存地址的寫請求(Remote Write),則我們看到狀態圖裏此時CPU A的Cache Line狀態為Invalid,其原因如下。
  CPU B此時發出的是一個特殊的請求——讀並且打算修改數據,當CPU A從總線上嗅探到這個請求後,會先阻止此請求並取得總線的控製權(Takes Control of Bus),隨後將Cache Line裏修改過的數據回寫道Memory中,再將此Cache Line的狀態修改為Invalid(這是因為其他CPU要改數據,所以沒必要改為Shared)。與此同時,CPU B發現之前的請求並沒有得到響應,於是重新發起一次請求,此時由於所有CPU的Cache裏都沒有內存副本了,所以CPU B的Cache就從Memory中加載最新的數據到Cache Line中,隨後修改數據,然後改變Cache Line的狀態為Modified。
  (4)如果內存中的某個變量被多個CPU加載到各自的Cache中,從而使得變量對應的Cache Line狀態為Shared,若此時某個CPU打算對此變量進行寫操作,則會導致所有擁有此變量緩存的CPU的Cache Line狀態都變為Invalid,這是引發性能下降的一種典型Cache Miss問題。
  在理解了MESI協議以後,我們明白了一個重要的事實,即存在多個處理器時,對共享變量的修改操作會涉及多個CPU之間的協調問題及Cache失效問題,這就引發了著名的“Cache偽共享”問題。
  下麵我們說說緩存命中的問題。如果要訪問的數據不在CPU的運算單元裏,則需要從緩存中加載,如果緩存中恰好有此數據而且數據有效,就命中一次(Cache Hit),反之產生一次Cache Miss,此時需要從下一級緩存或主存中再次嚐試加載。根據之前的分析,如果發生了Cache Miss,則數據的訪問性能瞬間下降很多!在我們需要大量加載運算的情況下,數據結構、訪問方式及程序算法方麵是否符合“緩存友好”的設計,就成為“量變引起質變”的關鍵性因素了。這也是為什麼最近,國外很多大數據領域的專家都熱衷於研究設計和采用新一代的數據結構和算法,而其核心之一就是“緩存友好”。

3 著名的Cache偽共享問題

  Cache偽共享問題是編程中真實存在的一個問題,考慮如下所示的Java Class結構:

class MyObject 
{
 private long  a;
private long  b;
private long c;
}

  按照Java規範,MyObject的對象是在堆內存上分配空間存儲的,而且a、b、c三個屬性在內存空間上是近鄰,如下所示。
         圖片描述
  我們知道,X86的CPU中Cache Line的長度為64字節,這也就意味著MyObject的3個屬性(長度之和為24字節)是完全可能加載在一個Cache Line裏的。如此一來,如果我們有兩個不同的線程(分別運行在兩個CPU上)分別同時獨立修改a與b這兩個屬性,那麼這兩個CPU上的Cache Line可能出現如下所示的情況,即a與b這兩個變量被放入同一個Cache Line裏,並且被兩個不同的CPU共享。
              圖片描述
  根據MESI協議的相關知識,我們知道,如果Thread 0要對a變量進行修改,則因為CPU 1上有對應的Cache Line,這會導致CPU 1的Cache Line無效,從而使得Thread 1被迫重新從Memory裏獲取b的內容(b並沒有被其他CPU改變,這樣做是因為b與a在一個Cache Line裏)。同樣,如果Thread 1要對b變量進行修改,則同樣導致Thread 0的Cache Line失效,不得不重新從Memory裏加載a。如此一來,本來是邏輯上無關的兩個線程,完全可以在兩個不同的CPU上同時執行,但陰差陽錯地共享了同一個Cache Line並相互搶占資源,導致並行成為串行,大大降低了係統的並發性,這就是所謂的Cache偽共享。
  解決Cache偽共享問題的方法很簡單,將a與b兩個變量分到不同的Cache Line裏,通常可以用一些無用的字段填充a與b之間的空隙。由於偽共享問題對性能的影響比較大,所以JDK 8首次提供了正式的普適性的方案,即采用@Contended注解來確保一個Object或者Class裏的某個屬性與其他屬性不在一個CacheLine裏,下麵的VolatileLong的多個實例之間就不會產生Cache偽共享的問題:

@Contended
class VolatileLong {
  public volatile long value = 0L; 
}

4 深入理解不一致性內存

  MESI協議解決了多核CPU下的Cache一致性問題,因而成為SMP架構的唯一選擇。SMP架構近幾年迅速在PC領域(X86)發展,一個CPU芯片上集成的CPU核心數量越來越多,到2017年,AMD的ZEN係列處理器就已經達到16核心32線程了。SMP架構是一種平行的結果,所有CPU Core都連接到一個內存總線上,它們平等訪問內存,同時整個內存是統一結構、統一尋址的(Uniform Memory Architecture,UMA)。如下所示給出了SMP架構的示意圖。
                  圖片描述
  但是,隨著CPU核心數量的不斷增長,SMP架構也暴露出其天生的短板,其根本瓶頸是共享內存總線的帶寬無法滿足CPU數量的增加,同時,一條“馬路”上通行的“車”多了,難免陷入“擁堵模式”。在這種情況下,分布式解決方案應運而生,係統的內存與CPU進行分割並捆綁在一起,形成多個獨立的子係統,這些子係統之間高速互連,這就是所謂的NUMA(None Uniform Memory Architecture)架構,如下圖所示。
  圖片描述
  我們可以認為NUMA架構第1次打破了“大鍋飯”的模式,內存不再是一個整體,而是被分割為相互獨立的幾塊,被不同的CPU私有化(Attach到不同的CPU上)。因此,當CPU訪問自身私有的內存地址時(Local Access),會很快得到響應,而如果需要訪問其他CPU控製的內存數據(Remote Access),則需要通過某種互連通道(Inter-connect通道)訪問,響應時間與之前相比變慢。 NUMA 的主要優點是伸縮性,NUMA的這種體係結構在設計上已經超越了SMP,可以擴展到幾百個CPU而不會導致性能嚴重下降。
  NUMA技術最早出現在20世紀80年代,主要運行在一些大中型UNIX係統中,Sequent公司是世界公認的NUMA技術領袖。早在1986年,Sequent公司就率先利用微處理器構建大型係統,開發了基於UNIX的SMP體係結構,開創了業界轉入SMP領域的先河。1999年9月,IBM公司收購了Sequent公司,將NUMA技術集成到IBM UNIX陣營中,並推出了能夠支持和擴展Intel平台的NUMA-Q係統及解決方案,為全球大型企業客戶適應高速發展的電子商務市場提供了更加多樣化、高可擴展性及易於管理的選擇,成為NUMA技術的領先開發者與革新者。隨後很多老牌UNIX服務器廠商也采用了NUMA技術,例如IBM、Sun、惠普、Unisys、SGI等公司。2000年全球互聯網泡沫破滅後,X86+Linux係統開始以低廉的成本侵占UNIX的地盤,AMD率先在其AMD Opteron係列處理器中的X86 CPU上實現了NUMA架構,Intel也跟進並在Intel Nehalem中實現了NUMA架構(Intel服務器芯片誌強E5500以上的CPU和桌麵的i3、i5、i7均基於此架構),至此NUMA這個貴族技術開始真正走入平常百姓家。
  下麵我們詳細分析一下NUMA技術的特點。首先,NUMA架構中引入了一個重要的新名詞——Node,一個Node由一個或者多個Socket Socket組成,即物理上的一個或多個CPU芯片組成一個邏輯上的Node。如下所示為來自Dell PowerEdge係列服務器的說明手冊中的NUMA的圖片,4個Intel Xeon E5-4600處理器形成4個獨立的NUMA Node,由於每個Intel Xeon E5-4600為8 Core,支持雙線程,所以每個Node裏的Logic CPU數量為16個,占每個Node分配係統總內存的1/4,每個Node之間通過Intel QPI(QuickPath Interconnect)技術形成了點到點的全互連處理器係統。
          圖片描述
  其次,我們看到NUMA這種基於點到點的全互連處理器係統與傳統的基於共享總線的處理器係統的SMP還是有巨大差異的。在這種情況下無法通過嗅探總線的方式來實現Cache一致性,因此為了實現NUMA架構下的Cache一致性,Intel引入了MESI協議的一個擴展協議——MESIF。MESIF采用了一種基於目錄表的實現方案,該協議由Boxboro-EX處理器係統實現,但獨立研究MESIF協議並沒有太大的意義,因為目前Intel並沒有公開Boxboro-EX處理器係統的詳細設計文檔。
  最後,我們說說NUMA架構的當前困境與我們對其未來的展望。
  NUMA架構由於打破了傳統的“全局內存”概念,目前在編程語言方麵還沒有任何一種語言從內存模型上支持它,所以當前很難開發適應NUMA的軟件。但這方麵已經有很多嚐試和進展了。Java在支持NUMA的係統裏,可以開啟基於NUMA的內存分配方案,使得當前線程所需的內存從對應的Node上分配,從而大大加快對象的創建過程。在大數據領域,NUMA係統正發揮著越來越強大的作用,SAP的高端大數據係統HANA被SGI在其UV NUMA Systems上實現了良好的水平擴展。據說微軟將會把SQL Server引入到Linux上,如此一來,很多潛在客戶將有機會在SGI提供的大型NUMA機器上高速運行多個SQL Server實例。在雲計算與虛擬化方麵,OpenStack與VMware已經支持基於NUMA技術的虛機分配能力,使得不同的虛機運行在不同的Core上,同時虛機的內存不會跨越多個NUMA Node。
  NUMA技術也會推進基於多進程的高性能單機分布式係統的發展,即在4個Socket、每個Socket為16Core的強大機器裏,隻要啟動4個進程,通過NUMA技術將每個進程綁定到一個Socket上,並保證每個進程隻訪問不超過Node本地的內存,即可讓係統進行最高性能的並發,而進程間的通信通過高性能進程間的通信技術實現即可。
  本文選自《架構解密:從分布式到微服務》,點此鏈接可在博文視點官網查看此書。
                  圖片描述
想及時獲得更多精彩文章,可在微信中搜索“博文視點”或者掃描下方二維碼並關注。
                     圖片描述

最後更新:2017-07-24 14:02:40

  上一篇:go  gcc 0級優化的重要性
  下一篇:go  vue初步介紹