深入淺析內存


title: 深入淺析內存
categories: java
tags: [內存,java]
description: 最近在看一本<<架構解密:從分佈式到微服務>>中看到了有關內存的相關知識,自己這方面比較薄弱,就想着記錄下來。書本地址:www.zhihu.com/pub/book/119572875


深入淺析內存

除了CPU,內存大概是最重要的計算資源了。基本稱爲分佈式系統標配的緩存中間件、高性能的數據處理系統及當前流行的大數據平臺,都離不開對計算機內存的深入理解與巧妙使用。

你所不知道的內存知識

複雜的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。

    然後,我們先從第一個非常簡單的問題開始:CPU可以直接操作內存嗎?可能99%的程序員會不假思索的回答:「肯定的,不然程序怎麼跑。」如果理性地分析一下,你會發現這個回答有問題:CPU與內存條是獨立的兩個硬件,而且CPU上也沒有插槽和連線可以讓內存條掛上去,也就是說,CPU並不能直接訪問內存條,而是要通過主辦上的其他硬件(接口)來間接訪問內存條。

   第二個問題:CPU的運算速度與內存條的訪問速度在回見的差距究竟有多大?這個差距跟王健林「先掙它個一個億的」小目標和「普通人有車有房」的宏大目標之間的差距相比,是更大還是更小呢?答案是:「差不多」。通常來說,CPU的運算速度與內存訪問速度之間的差距不過是100倍,假如有100萬RMB就可以有有房(貸)有車(貸)了沒那麼其100倍就剛好是一億RMB。

   既然CPU的速度與內存的速度還是存在高度兩個數量級的巨大鴻溝,所有它們註定不能「幸福地在一起」,於是CPU的親密伴侶Cache閃亮登場。與來自DRAM家族的內存(Memory)出身不同,Cache來自ASRAM家族。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)們層層轉發「聖旨」(內部指令),最紅抵達「後宮」(內存).

在這裏插入圖片描述


多核CPU與內存共享的問題

   在多核CPU的情況下,如何共享內存?

   如果擅長多線程高級編程,那麼肯定會毫不猶豫地給出以下僞代碼:

synchronized(memory){
    doSomething(...);
}

   如果真這個簡單,那麼這個世界上就不會只剩下兩家獨大的主流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)發起本地寫請求(Loacl 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)以上面第二條內容爲基礎,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,則數據的訪問性能瞬間下降很多!在我們需要大量加載運算的情況下,數據結構、訪問方式及程序運算方面是否符合「緩存友好」的設計,就成爲「量變引起質變」得關鍵性因素了。這也是爲什麼最近,國外很多大數據領域的專家都熱衷於研究設計和採用新一代的數據結構和算法,而其核心之一就是「緩存友好」。


著名的Cache僞共享問題

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

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

    按照java規範,MyObject的對象是在堆內存上分配空間存儲的,而且a、b、c三個屬性在內存空間上是鄰近,如下所示。

a(8個字節) b(8個字節) c(8個字節)

   我們知道,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裏的某個屬性與其他屬性不在一個Cache Line裏,下面的VolatileLong的多個實例之間就不會產生Cache僞共享的問題:

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

深入理解不一致性內存

   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組成,即物理上的一個或多個CPU芯片組成一個邏輯上的Node。如下所示爲來自Dell PowerEdge系統服務器的說明手冊中的NUMA的圖片,4個Intel Xeon E 5-4600處理器形成4個獨立的NUMA Node,由於每個Intel Xeon E 5-4600爲8Core,支持雙線程,所以每個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並沒有公開Boxbore-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本地的內存,即可讓系統進行最高性能的併發,而進程間的通信通過高性能進程間的通信技術實現即可。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章