What every programmer should know about memory 筆記

What every programmer should know about memory, Part 1(筆記)

2.商用硬件現狀
     如今硬件的組成對於pc機而言基本上都是一下的結構:
     
由2部分組成:南橋,北橋
CPU經過FSB(前端總線)鏈接到北橋芯片,北橋芯片主要包含內存控制器和其餘一些組件,內存控制器決定了內存的類型,SDRAM,DRAM等都須要不一樣類型的內存控制器。
南橋芯片主要是經過多條不一樣的總線和設備通訊,主要有PCI,SATA,USB等還支持PATA,IEEE 1394,串口和並口。
須要注意一下地方:
     1.cpu之間的通訊須要經過它與北橋之間的鏈接總線
     2.與RAM的通訊須要走北橋芯片
     3.RAM只有一個端口
     4.CPU和南橋設備通訊須要通過北橋
第一個瓶頸:早期RAM的訪問都要通過cpu,影響了總體性能,因此出現了DMA(直接內存訪問),無需CPU干涉可是會致使和CPU爭奪北橋的帶寬。
第二個瓶頸:北橋和RAM之間的總線因此出現了雙通道(能夠實現帶寬加倍,內存訪問在兩個通道上交錯分配)
除了併發訪問模式也是有瓶頸的看2.2
比較昂貴的系統上可能會出現:
北橋自身不帶內存控制器,而是鏈接到外部多個內存控制器上,好處是支持更多的內存,能夠同時訪問不一樣的內存區,下降了延遲,可是對北橋的內部帶寬要求巨大。
使用外部內存控制器並非惟一的辦法,比較流行的還有一種是把控制器集成到cpu內部,將內存直接鏈接到CPU
這樣的架構,系統裏有幾個cpu就能夠有幾個內存庫(memory bank),不須要強大的北橋就能夠實現4倍的內存帶寬。可是缺點也是很明顯:1.致使內存再也不是統一的資源(NUMA的得名),2.cpu能夠正常的訪問本地內存,可是訪問其餘內存時須要和其餘cpu互通。 在討論訪問遠端內存的代價時,咱們用「NUMA因子」這個詞。 好比說IBM的x445和SGI的Altix系列。CPU被納入節點,節點內的內存訪問時間是一致的,或者只有很小的NUMA因子。而在節點之間的鏈接代價很大,並且有巨大的NUMA因子。
2.1 RAM類型
RAM主要分爲2中靜態RAM,動態RAM,前者速度快,代價搞,後者速度慢代價低
2.1.1靜態RAM
主要有6個晶體管組成,核心是4個晶體管M1-M4,他們有2個穩定狀態分別表明0和1
2.1.2 動態RAM
動態RAM只有一個晶體管和一個電容
 
動態RAM優勢是簡單,可是缺點是因爲讀取狀態時須要對電容器放電,因此這一過程不能無限重複,不得不在某個點上對它從新充電。更糟糕的是,爲了容納大量單元(如今通常在單個芯片上容納10的9次方以上的RAM單元),電容器的容量必須很小(0.000000000000001法拉如下)。這樣,完整充電後大約持有幾萬個電子。即便電容器的電阻很大(若干兆歐姆),仍然只需很短的時間就會耗光電荷,稱爲「泄漏」。這種泄露就是如今的大部分DRAM芯片每隔64ms就必須進行一次刷新的緣由。(附A關於三極管的輸入輸出特性)
2.2 DRAM訪問細節
同步DRAM,顧名思義,是參照一個時間源工做的。由內存控制器提供一個時鐘,時鐘的頻率決定了前端總線(FSB)的速度。以今天的SDRAM爲例,每次數據傳輸包含64位,即8字節。因此FSB的傳輸速率應該是有效總線頻率乘於8字節(對於4倍傳輸200MHz總線而言,傳輸速率爲6.4GB/s)。聽起來很高,但要知道這只是峯值速率,實際上沒法達到的最高速率。咱們將會看到,與RAM模塊交流的協議有大量時間是處於非工做狀態,不進行數據傳輸。咱們必須對這些非工做時間有所瞭解,並儘可能縮短它們,才能得到最佳的性能。
2.2.1讀訪問協議
 
這裏忽略了許多細節,咱們只關注時鐘頻率、RAS與CAS信號、地址總線和數據總線。首先,內存控制器將行地址放在地址總線上,並下降RAS信號,讀週期開始。全部信號都在時鐘(CLK)的上升沿讀取,所以,只要信號在讀取的時間點上保持穩定,就算不是標準的方波也沒有關係。設置行地址會促使RAM芯片鎖住指定的行。
 
CAS信號在tRCD(RAS到CAS時延)個時鐘週期後發出。內存控制器將列地址放在地址總線上,下降CAS線。這裏咱們能夠看到,地址的兩個組成部分是怎麼經過同一條總線傳輸的。
 
既然數據的傳輸須要這麼多的準備工做,僅僅傳輸一個字顯然是太浪費了。所以,DRAM模塊容許內存控制指定本次傳輸多少數據。能夠是二、4或8個字。這樣,就能夠一次填滿高速緩存的整條線,而不須要額外的RAS/CAS序列。另外,內存控制器還能夠在不重置行選擇的前提下發送新的CAS信號。這樣,讀取或寫入連續的地址就能夠變得很是快,由於不須要發送RAS信號,也不須要把行置爲非激活狀態(見下文)。
 
在上圖中,SDRAM的每一個週期輸出一個字的數據。這是第一代的SDRAM。而DDR能夠在一個週期中輸出兩個字。這種作法能夠減小傳輸時間,但沒法下降時延。
 
2.2.2預充電和激活
2.2.1中的圖只是讀取數據的一部分,還有如下部分:
顯示的是兩次CAS信號的時序圖。第一次的數據在CL週期後準備就緒。圖中的例子裏,是在SDRAM上,用兩個週期傳輸了兩個字的數據。若是換成DDR的話,則能夠傳輸4個字。即便是在一個命令速率爲1的DRAM模塊上,也沒法當即發出預充電命令,而要等數據傳輸完成。在上圖中,即爲兩個週期。恰好與CL相同,但只是巧合而已。預充電信號並無專用線,某些實現是用同時下降寫使能(WE)線和RAS線的方式來觸發。
 
發出預充電信命令後,還需等待tRP(行預充電時間)個週期以後才能使行被選中。在圖2.9中,這個時間(紫色部分)大部分與內存傳輸的時間(淡藍色部分)重合。不錯。但tRP大於傳輸時間,所以下一個RAS信號只能等待一個週期。
 
數據總線的7個週期中只有2個週期纔是真正在用的。再用它乘於FSB速度,結果就是,800MHz總線的理論速率6.4GB/s降到了1.8GB/s
 
咱們會看到預充電指令被數據傳輸時間限制(途中爲COL Addr的傳輸)除此以外,SDRAM模塊在RAS信號以後,須要通過一段時間,才能進行預充電(記爲tRAS)( minimum active to precharge time(也就是RAS信號以後到充電的最小時間間隔))它的值很大,通常達到tRP的2到3倍。若是在某個RAS信號以後,只有一個CAS信號,並且數據只傳輸不多幾個週期,那麼就有問題了。假設在圖2.9中,第一個CAS信號是直接跟在一個RAS信號後免的,而tRAS爲8個週期。那麼預充電命令還須要被推遲一個週期,由於tRCD、CL和tRP加起來才7個週期。
 
DDR模塊每每用w-z-y-z-T來表示。例如,2-3-2-8-T1,意思是:
 
w 2 CAS時延(CL) 
x 3 RAS-to-CAS時延(t RCD) 
y 2 RAS預充電時間(t RP) 
z 8 激活到預充電時間(t RAS) 
T T1 命令速率
 
2.2.3重充電
充電對內存是性能最大的影響,根據JEDEC規範,DRAM單元必須保持每64ms刷新一次咱們在解讀性能參數時有必要知道,它也是DRAM生命週期的一個部分。若是系統須要讀取某個重要的字,而恰好它所在的行正在刷新,那麼處理器將會被延遲很長一段時間。刷新的具體耗時取決於DRAM模塊自己。
 
2.2.4內存類型
如下是SDR(SDRAME)的操做圖比較簡單
DRAM陣列的頻率和總線的頻率保持相同,可是當全部組件頻率上升時,那麼致使耗電量也上升代價很大,因此就出了DDR,在不提升頻率的情況下提升吞吐量
SDR和DDR1的區別就是DDR1能夠在上升沿和降低沿都傳輸數據,實現了雙倍的傳輸。DDR引入了一個緩衝區。緩衝區的每條數據線都持有兩位。它要求內存單元陣列的數據總線包含兩條線。實現的方式很簡單,用同一個列地址同時訪問兩個DRAM單元。對單元陣列的修改也很小。
爲了進一步,因而有了DDR2,最明顯的變化是,總線的頻率加倍了。頻率的加倍意味着帶寬的加倍。若是對單元陣列的頻率加倍,顯然是不經濟的,所以DDR2要求I/O緩衝區在每一個時鐘週期讀取4位。也就是說,DDR2的變化僅在於使I/O緩衝區運行在更高的速度上。這是可行的,並且耗電也不會顯著增長。DDR2的命名與DDR1相仿,只是將因子2替換成4
陣列頻率 總線頻率 數據率 名稱(速率) 名稱
(FSB)
133MHz 266MHz 4,256MB/s PC2-4200 DDR2-533
166MHz 333MHz 5,312MB/s PC2-5300 DDR2-667
200MHz 400MHz 6,400MB/s PC2-6400 DDR2-800
250MHz 500MHz 8,000MB/s PC2-8000 DDR2-1000
266MHz 533MHz 8,512MB/s PC2-8500 DDR2-1066
FSB速度是用有效頻率來標記的,即把上升、降低沿均傳輸數據的因素考慮進去,所以數字被撐大了。因此,擁有266MHz總線的133MHz模塊有着533MHz的FSB「頻率」
 
DDR3變化更多,電壓從1.8降低到了1.5因而耗電也變小了,或者說保持相同的耗電,ddr3能夠達到更高的頻率或者,保持一樣的熱能釋放,實現容量翻番
 
DDR3模塊的單元陣列將運行在內部總線的四分之一速度上,DDR3的I/O緩衝區從DDR2的4位提高到8位
 
DDR3可能會有一個問題,即在1600Mb/s或更高速率下,每一個通道的模塊數可能會限制爲1。在早期版本中,這一要求是針對全部頻率的。咱們但願這個要求能夠提升一些,不然系統容量將會受到嚴重的限制。
咱們預計中各DDR3模塊的名稱。JEDEC目前贊成了前四種。因爲Intel的45nm處理器是1600Mb/s的FSB,1866Mb/s能夠用於超頻市場。隨着DDR3的發展,可能會有更多類型加入
陣列頻率 總線頻率 數據速率 名稱(速率) 名稱
(FSB)
100MHz 400MHz 6,400MB/s PC3-6400 DDR3-800
133MHz 533MHz 8,512MB/s PC3-8500 DDR3-1066
166MHz 667MHz 10,667MB/s PC3-10667 DDR3-1333
200MHz 800MHz 12,800MB/s PC3-12800 DDR3-1600
233MHz 933MHz 14,933MB/s PC3-14900 DDR3-1866
全部的DDR內存都有一個問題:不斷增長的頻率使得創建並行數據總線變得十分困難。一個DDR2模塊有240根引腳。全部到地址和數據引腳的連線必須被佈置得差很少同樣長。更大的問題是,若是多於一個DDR模塊經過菊花鏈鏈接在同一個總線上,每一個模塊所接收到的信號隨着模塊的增長會變得愈來愈扭曲。
 
DDR2規範容許每條總線(又稱通道)鏈接最多兩個模塊,DDR3在高頻率下只容許每一個通道鏈接一個模塊。每條總線多達240根引腳使得單個北橋沒法以合理的方式驅動兩個通道。替代方案是增長外部內存控制器,但這會提升成本。
一種解法是,在處理器中加入內存控制器
另一種是,Intel針對大型服務器方面的解法(至少在將來幾年),是被稱爲全緩衝DRAM(FB-DRAM)的技術。FB-DRAM採用與DDR2相同的器件,所以造價低廉。不一樣之處在於它們與內存控制器的鏈接方式。FB-DRAM沒有用並行總線,而用了串行總線。串行總線能夠達到更高的頻率,串行化的負面影響,甚至能夠增長帶寬。使用串行總線後
 
  1. 每一個通道能夠使用更多的模塊。
  2. 每一個北橋/內存控制器能夠使用更多的通道。
  3. 串行總線是全雙工的(兩條線)。
FB-DRAM只有69個腳。經過菊花鏈方式鏈接多個FB-DRAM也很簡單。FB-DRAM規範容許每一個通道鏈接最多8個模塊。在對比下雙通道北橋的鏈接性,採用FB-DRAM後,北橋能夠驅動6個通道,並且腳數更少——6x69對比2x240。每一個通道的佈線也更爲簡單,有助於下降主板的成本。全雙工的並行總線過於昂貴。而換成串行線後,這再也不是一個問題,所以串行總線按全雙工來設計的,這也意味着,在某些狀況下,僅靠這一特性,總線的理論帶寬已經翻了一倍。還不止於此。因爲FB-DRAM控制器可同時鏈接6個通道,所以能夠利用它來增長某些小內存系統的帶寬。對於一個雙通道、4模塊的DDR2系統,咱們能夠用一個普通FB-DRAM控制器,用4通道來實現相同的容量。串行總線的實際帶寬取決於在FB-DRAM模塊中所使用的DDR2(或DDR3)芯片的類型。

DDR2
FB-DRAM
240 69
通道 2 6
每通道DIMM數 2 8
最大內存 16GB 192GB
吞吐量 ~10GB/s ~40GB/s
 
2.2.5 結論
經過本節,你們應該瞭解到訪問DRAM的過程並非一個快速的過程。至少與處理器的速度相比,或與處理器訪問寄存器及緩存的速度相比,DRAM的訪問不算快。你們還須要記住CPU和內存的頻率是不一樣的。Intel Core 2處理器運行在2.933GHz,而1.066GHz FSB有11:1的時鐘比率(注: 1.066GHz的總線爲四泵總線)。那麼,內存總線上延遲一個週期意味着處理器延遲11個週期。絕大多數機器使用的DRAM更慢,所以延遲更大。
前文中讀命令的時序圖代表,DRAM模塊能夠支持高速數據傳輸。每一個完整行能夠被毫無延遲地傳輸。數據總線能夠100%被佔。對DDR而言,意味着每一個週期傳輸2個64位字。對於DDR2-800模塊和雙通道而言,意味着12.8GB/s的速率。
 
可是,除非是特殊設計,DRAM的訪問並不老是串行的。訪問不連續的內存區意味着須要預充電和RAS信號。因而,各類速度開始慢下來,DRAM模塊急需幫助。預充電的時間越短,數據傳輸所受的懲罰越小。
 
硬件和軟件的預取(參見第6.3節)能夠在時序中製造更多的重疊區,下降延遲。預取還能夠轉移內存操做的時間,從而減小爭用。咱們經常遇到的問題是,在這一輪中生成的數據須要被存儲,而下一輪的數據須要被讀出來。經過轉移讀取的時間,讀和寫就不須要同時發出了
 
2.3主存的其餘用戶
除了CPU外,系統中還有其它一些組件也能夠訪問主存。高性能網卡或大規模存儲控制器是沒法承受經過CPU來傳輸數據的,它們通常直接對內存進行讀寫(直接內存訪問,DMA)。在圖2.1中能夠看到,它們能夠經過南橋和北橋直接訪問內存。另外,其它總線,好比USB等也須要FSB帶寬,即便它們並不使用DMA,但南橋仍要經過FSB鏈接到北橋。
 
DMA固然有很大的優勢,但也意味着FSB帶寬會有更多的競爭。在有大量DMA流量的狀況下,CPU在訪問內存時必然會有更大的延遲。咱們能夠用一些硬件來解決這個問題。例如,經過圖2.3中的架構,咱們能夠挑選不受DMA影響的節點,讓它們的內存爲咱們的計算服務。還能夠在每一個節點上鍊接一個南橋,將FSB的負荷均勻地分擔到每一個節點上。
 
附A
三極管 有3個極1.發射極(e),基極(b),集電極(c)
輸入特性:當b輸入必定電壓是,發射端導通
輸出特性:當c,e之間加點壓,電流大小,有基極輸入電流控制c和e之間的電流大小
這樣對動態內存單元中的電容來講每次讀取就是一個放過程,因此讀完以後要從新充電
3.1 高速緩存的位置
 
早期的一些系統就是相似的架構。在這種架構中,CPU核心再也不直連到主存。數據的讀取和存儲都通過高速緩存。主存與高速緩存都連到系統總線上,這條總線同時還用於與其它組件通訊。咱們管這條總線叫「FSB」.
在高速緩存出現後不久,系統變得更加複雜。高速緩存與主存之間的速度差別進一步拉大,直到加入了另外一級緩存。新加入的這一級緩存比第一級緩存更大,可是更慢。因爲加大一級緩存的作法從經濟上考慮是行不通的,因此有了二級緩存,甚至如今的有些系統擁有三級緩存.
L1d是一級數據緩存,L1i是一級指令緩存,等等。請注意,這只是示意圖, 真正的數據流並不須要流經上級緩存。CPU的設計者們在設計高速緩存的接口時擁有很大的自由。
咱們有多核CPU,每一個核心能夠有多個「線程」。核心與線程的不一樣之處在於,核心擁有獨立的硬件資源
3.2 高級的緩存操做
默認狀況下,CPU核心全部的數據的讀或寫都存儲在緩存中。固然,也有內存區域不能被緩存的
若是CPU須要訪問某個字(word),先檢索緩存。很顯然,緩存不可能容納主存全部內容(不然還須要主存幹嗎)。系統用字的內存地址來對緩存條目進行標記。若是須要讀寫某個地址的字,那麼根據標籤來檢索緩存便可(後面會介紹還會使用地址來計算緩存的地址)
標籤是須要額外空間的,用字做爲緩存的粒度顯然毫無效率。由於標籤可能也有32位(x86上)。內存模塊在傳輸位於同一行上的多份數據時,因爲不須要發送新CAS信號,甚至不須要發送RAS信號,所以能夠實現很高的效率。基於以上的緣由,緩存條目並不存儲單個字,而是存儲若干連續字組成的「線」。在早期的緩存中,線長是32字節,如今通常是64字節。對於64位寬的內存總線,每條線須要8次傳輸。而DDR對於這種傳輸模式的支持更爲高效。
當處理器須要內存中的某塊數據時,整條緩存線被裝入L1d。緩存線的地址經過對內存地址進行掩碼操做生成。對於64字節的緩存線,是將低6位置0。這些被丟棄的位做爲線內偏移量。其它的位做爲標籤,並用於在緩存內定位。在實踐中,咱們將地址分爲三個部分。32位地址的狀況以下:
若是緩存線長度爲2O,那麼地址的低O位用做線內偏移量。上面的S位選擇「緩存集」。後面咱們會說明使用緩存集的緣由。如今只須要明白一共有2S個緩存集就夠了。剩下的32 - S - O = T位組成標籤。它們用來同一個cache set中的各條線
當某條指令修改內存時,仍然要先裝入緩存線,由於任何指令都不可能同時修改整條線。所以須要在寫操做前先把緩存線裝載進來。若是緩存線被寫入,但尚未寫回主存,那就是所謂的「髒了」。髒了的線一旦寫回主存,髒標記即被清除。爲了裝入新數據,基本上老是要先在緩存中清理出位置。L1d將內容逐出L1d,推入L2(線長相同)。固然,L2也須要清理位置。因而L2將內容推入L3,最後L3將它推入主存。這種逐出操做一級比一級昂貴。(因此AMD公司一般使用exclusive caches見 附錄1,Intel使用inclusive cache)
在對稱多處理器(SMP)架構的系統中,CPU的高速緩存不能獨立的工做。在任什麼時候候,全部的處理器都應該擁有相同的內存內容。保證這樣的統一的內存視圖被稱爲「高速緩存一致性」從一個處理器直接訪問另外一個處理器的高速緩存這種模型設計代價將是很是昂貴的,它是一個至關大的瓶頸。而是使用,當另外一個處理器要讀取或寫入到高速緩存線上時,處理器會去檢測。 
若是CPU檢測到一個寫訪問,並且該CPU的cache中已經緩存了一個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的寫訪問會致使第二個cpu的cache line的全部拷貝被標記爲無效。
對於寫入,cpu不須要等待安全的寫入,只要能模擬這個效果,cpu就能夠走捷徑
如下是隨機寫入的圖:
 
圖中有三個比較明顯的不一樣階段。很正常,這個處理器有L1d和L2,沒有L3。根據經驗能夠推測出,L1d有2**13字節,而L2有2**20字節。由於,若是整個工做集均可以放入L1d,那麼只需不到10個週期就能夠完成操做。若是工做集超過L1d,處理器不得不從L2獲取數據,因而時間飄升到28個週期左右。若是工做集更大,超過了L2,那麼時間進一步暴漲到480個週期以上。這時候,許多操做將不得不從主存中獲取數據。更糟糕的是,若是修改了數據,還須要將這些髒了的緩存線寫回內存。( 爲何工做集超過了L1d就會從L2中取數若爲讀,當讀入的數據不在L1就要向L2獲取,不在L2就要想L3或者內存獲取,工做集越大,致使L1,L2被填滿。如果寫同理學入到髒數據存放在L1中,時間一長L1填滿依次類推
3.3 CPU緩存實現的細節
3.3.1 關聯性
咱們可讓緩存的每條線能存聽任何內存地址的數據。這就是所謂的全關聯緩存(fully associative cache)。這種緩存方式,若是處理器要訪問某條線,那麼須要全部的cacheline的tag和請求的地址比較。
全關聯:優勢,能夠聽任意地址的數據,不會出現相似直接映射同樣的情況,若是數據分佈不均勻致使被換出,從而致使miss增長
              缺點,當cpu給出一個地址訪問某一個緩存線,須要掃描全部的緩存
直接映射:優勢,電路簡單,查詢某個元素的速度很快。由於一個元素出如今cache的位子是固定的。
               缺點,若是數據分佈在同一個set那麼,會致使miss大大的提升
組關聯:這個是當前廣泛的使用方法。是直接映射和全關聯的折中辦法
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
 
表說明:關聯度,cache大小,cacheline大小對miss的影響。從上面數據得出的結論是CL64比CL32miss要少,cache越到miss越少,關聯度越高miss越少
圖是表數據的圖標化更容易看出,問題其中CL大小爲32。cache size越大miss越少,關聯越大miss 越少
在其餘文獻中提到說增長關聯度和增長緩存有相同的效果,這個固然是不正確的看圖中,4m-8m直接關聯和,2路關聯是有同樣的提高,可是當緩存愈來愈大就很差說了。測試程序的workset只有5.6M,使用8MB以後天然沒法體現優點。可是當workset愈來愈大,小緩存的關聯性就體現出了巨大的優點。
隨着多核cpu的出現,相對來講cache的大小就被平分,所以關聯性就顯得比較重要。可是已經到了16路關聯,若是再加顯然比較困難,因此就有廠家開始考慮使用3級緩存。
3.3.2 Cache的性能測試
用於測試程序的數據能夠模擬一個任意大小的工做集:包括讀、寫訪問,隨機、連續訪問。在圖3.4中咱們能夠看到,程序爲工做集建立了一個與其大小和元素類型相同的數組:
  struct l {
    struct l *n;
    long int pad[NPAD];
  };
2**N 字節的工做集包含2**N/sizeof(struct l)個元素。顯然sizeof(struct l) 的值取決於NPAD的大小。在32位系統上,NPAD=7意味着數組的每一個元素的大小爲32字節,在64位系統上,NPAD=7意味着數組的每一個元素的大小爲64字節。( 關於如何CHECK,我還不知道)
單線程順序訪問
最簡單的狀況就是遍歷鏈表中順序存儲的節點。不管是從前向後處理,仍是從後向前,對於處理器來講沒有什麼區別。下面的測試中,咱們須要獲得處理鏈表中一個元素所須要的時間,以CPU時鐘週期最爲計時單元。圖顯示了測試結構。除非有特殊說明, 全部的測試都是在Pentium 4 64-bit 平臺上進行的,所以結構體l中NPAD=0,大小爲8字節。
                順序讀訪問, NPAD=0
                               順序讀多個字節
一開始的兩個測試數據收到了噪音的污染。因爲它們的工做負荷過小,沒法過濾掉系統內其它進程對它們的影響。咱們能夠認爲它們都是4個週期之內的。這樣一來,整個圖能夠劃分爲比較明顯的三個部分:
  • 工做集小於2**14字節的。
  • 工做集從2**15字節到2**20字節的。
  • 工做集大於2**21字節的。
這樣的結果很容易解釋——是由於處理器有16KB的L1d和1MB的L2。
L1d的部分跟咱們預想的差很少,在一臺P4上耗時爲4個週期左右。但L2的結果則出乎意料。你們可能以爲須要14個週期以上,但實際只用了9個週期。這要歸功於處理器先進的處理邏輯,當它使用連續的內存區時,會 預先讀取下一條緩存線的數據。這樣一來,當真正使用下一條線的時候,其實已經早已讀完一半了,因而真正的等待耗時會比L2的訪問時間少不少。
在工做集超過L2的大小以後,預取的效果更明顯了。前面咱們說過,主存的訪問須要耗時200個週期以上。但在預取的幫助下,實際耗時保持在9個週期左右。200 vs 9,效果很是不錯。
在L2階段,三條新加的線基本重合,並且耗時比老的那條線高不少,大約在28個週期左右,差很少就是L2的訪問時間。這代表,從L2到L1d的預取並無生效。這是由於,對於最下面的線(NPAD=0),因爲結構小,8次循環後才須要訪問一條新緩存線,而上面三條線對應的結構比較大,拿相對最小的NPAD=7來講,光是一次循環就須要訪問一條新線,更不用說更大的NPAD=15和31了。而預取邏輯是沒法在每一個週期裝載新線的,所以每次循環都須要從L2讀取,咱們看到的就是從L2讀取的時延。
(有一點想不通做者這裏說是28個週期是L2的訪問時間,可是上面爲何說是14個週期,有一種不靠譜的感受。元素大小太大致使預取效果不好,可是順序的訪問方式很容易被預取,爲何不沒有呢,做者的觀點是 預取邏輯是沒法在每一個週期裝載新線的因此致使預取無效)
另外一個致使慢下來的緣由是TLB緩存的未命中。TLB是存儲虛實地址映射的緩存。爲了保持快速,TLB只有很小的容量。若是有大量頁被反覆訪問,超出了TLB緩存容量,就會致使反覆地進行地址翻譯,這會耗費大量時間。TLB查找的代價分攤到全部元素上,若是元素越大,那麼元素的數量越少,每一個元素承擔的那一份就越多。
爲了觀察TLB的性能,咱們能夠進行另兩項測試。第一項:咱們仍是順序存儲列表中的元素,使NPAD=7,讓每一個元素佔滿整個cache line,第二項:咱們將列表的每一個元素存儲在一個單獨的頁上,忽略每一個頁沒有使用的部分以用來計算工做集的大小。結果代表,第一項測試中,每次列表的迭代都須要一個新的cache line,並且每64個元素就須要一個新的頁。第二項測試中,每次迭代都會訪問一個cache,都須要加載一個新頁。
           圖 3.12: TLB 對順序讀的影響
基於可用RAM空間的有限性,測試設置容量空間大小爲2的24次方字節,這就須要1GB的容量將對象放置在分頁上。NPAD等於7的曲線。咱們看到不一樣的步長顯示了高速緩存L1d和L2的大小。第二條曲線看上去徹底不一樣,其最重要的特色是當工做容量到達2的13次方字節時開始大幅度增加。這就是TLB緩存溢出的時候。咱們能計算出一個64字節大小的元素的TLB緩存有64個輸入。成本不會受頁面錯誤影響,由於程序鎖定了存儲器以防止內存被換出。能夠看出,計算物理地址並把它存儲在TLB中所花費的週期數量級是很是高的。從中能夠清楚的獲得:TLB緩存效率下降的一個重要因素是大型NPAD值的減緩。因爲物理地址必須在緩存行能被L2或主存讀取以前計算出來,地址轉換這個不利因素就增長了內存訪問時間。這一點部分解釋了爲何NPAD等於31時每一個列表元素的總花費比理論上的RAM訪問時間要高。
          圖3.13 NPAD等於1時的順序讀和寫
全部狀況下元素寬度都爲16個字節。第一條曲線「Follow」是熟悉的鏈表走線在這裏做爲基線。第二條曲線,標記爲「Inc」,僅僅在當前元素進入下一個前給其增長thepad[0]成員。第三條曲線,標記爲"Addnext0", 取出下一個元素的thepad[0]鏈表元素並把它添加爲當前鏈表元素的thepad[0]成員。
在沒運行時,你們可能會覺得"Addnext0"更慢,由於它要作的事情更多——在沒進到下個元素以前就須要裝載它的值。但實際的運行結果使人驚訝——在某些小工做集下,"Addnext0"比"Inc"更快。這是爲何呢?緣由在於, 系統通常會對下一個元素進行強制性預取。當程序前進到下個元素時這個元素其實早已被預取在L1d裏。可是,"Addnext0"比"Inc"更快離開L2,這是由於它須要從主存裝載更多的數據。而在工做集達到2 21字節時,"Addnext0"的耗時達到了28個週期,是同期"Follow"14週期的兩倍。這個兩倍也很好解釋。"Addnext0"和"Inc"涉及對內存的修改,所以L2的逐出操做不能簡單地把數據一扔了事,而必須將它們寫入內存。所以FSB的可用帶寬變成了一半,傳輸等量數據的耗時也就變成了原來的兩倍。
           圖3.14: 更大L2/L3緩存的優點
決定順序式緩存處理性能的另外一個重要因素是緩存容量。雖然這一點比較明顯,但仍是值得一說。圖中展現了128字節長元素的測試結果(64位機,NPAD=15)。此次咱們比較三臺不一樣計算機的曲線,兩臺P4,一臺Core 2。兩臺P4的區別是緩存容量不一樣,一臺是32k的L1d和1M的L2,一臺是16K的L1d、512k的L2和2M的L3。Core 2那臺則是32k的L1d和4M的L2。
圖中最有趣的地方,並非Core 2如何大勝兩臺P4,而是工做集開始增加到連末級緩存也放不下、須要主存熱情參與以後的部分。與咱們預計的類似,最末級緩存越大,曲線停留在L2訪問耗時區的時間越長。在2**20字節的工做集時,第二臺P4(更老一些)比第一臺P4要快上一倍,這要徹底歸功於更大的末級緩存。而Core 2拜它巨大的4M L2所賜,表現更爲卓越。
單線程隨機訪問模式的測量
           圖3.15: 順序讀取vs隨機讀取,NPAD=0
若是換成隨機訪問或者不可預測的訪問,狀況就大不相同了。圖3.15比較了順序讀取與隨機讀取的耗時狀況。
換成隨機以後,處理器沒法再有效地預取數據,只有少數狀況下靠運氣恰好碰到前後訪問的兩個元素挨在一塊兒的情形。
圖3.15中有兩個須要關注的地方。
首先,在大的工做集下須要很是多的週期。這臺機器訪問主存的時間大約爲200-300個週期,但圖中的耗時甚至超過了450個週期。咱們前面已經觀察到過相似現象(對比圖3.11)。這說明,處理器的自動預取在這裏起到了反效果。
其次,表明隨機訪問的曲線在各個階段不像順序訪問那樣保持平坦,而是不斷攀升。爲了解釋這個問題,咱們測量了程序在不一樣工做集下對L2的訪問狀況。結果如圖3.16和表3.2。
            圖3.16: L2d未命中率
Set
Size
Sequential Random
L2 Hit L2 Miss #Iter Ratio Miss/Hit L2 Accesses Per Iter L2 Hit L2 Miss #Iter Ratio Miss/Hit L2 Accesses Per Iter
220 88,636 843 16,384 0.94% 5.5 30,462 4721 1,024 13.42% 34.4
221 88,105 1,584 8,192 1.77% 10.9 21,817 15,151 512 40.98% 72.2
222 88,106 1,600 4,096 1.78% 21.9 22,258 22,285 256 50.03% 174.0
223 88,104 1,614 2,048 1.80% 43.8 27,521 26,274 128 48.84% 420.3
224 88,114 1,655 1,024 1.84% 87.7 33,166 29,115 64 46.75% 973.1
225 88,112 1,730 512 1.93% 175.5 39,858 32,360 32 44.81% 2,256.8
226 88,112 1,906 256 2.12% 351.6 48,539 38,151 16 44.01% 5,418.1
227 88,114 2,244 128 2.48% 705.9 62,423 52,049 8 45.47% 14,309.0
228 88,120 2,939 64 3.23% 1,422.8 81,906 87,167 4 51.56% 42,268.3
229 88,137 4,318 32 4.67% 2,889.2 119,079 163,398 2 57.84% 141,238.5
               
       表3.2: 順序訪問與隨機訪問時L2命中與未命中的狀況,NPAD=0
從圖中能夠看出,當工做集大小超過L2時,未命中率(L2未命中次數/L2訪問次數)開始上升。整條曲線的走向與圖3.15有些類似: 先急速爬升,隨後緩緩下滑,最後再度爬升。它與耗時圖有緊密的關聯。L2未命中率會一直爬升到100%爲止。只要工做集足夠大(而且內存也足夠大),就能夠將緩存線位於L2內或處於裝載過程當中的可能性降到很是低。
(工做集越大,隨機訪問,命中率就會越小)
           圖3.17: 頁意義上(Page-Wise)的隨機化,NPAD=7
而換成隨機訪問後,單位耗時的增加超過了工做集的增加,根源是TLB未命中率的上升。圖3.17描繪的是NPAD=7時隨機訪問的耗時狀況。這一次,咱們修改了隨機訪問的方式。正常狀況下是把整個列表做爲一個塊進行隨機(以∞表示),而其它11條線則是在小一些的塊裏進行隨機。例如,標籤爲'60'的線表示以60頁(245760字節)爲單位進行隨機。先遍歷完這個塊裏的全部元素,再訪問另外一個塊。這樣一來,能夠保證任意時刻使用的TLB條目數都是有限的。 (也就是上圖的性能差距主要來自於TLB的未命中率)
 
NPAD=7對應於64字節,正好等於緩存線的長度。因爲元素順序隨機,硬件預取不可能有任何效果,特別是在元素較多的狀況下。這意味着,分塊隨機時的L2未命中率與整個列表隨機時的未命中率沒有本質的差異。隨着塊的增大,曲線逐漸逼近整個列表隨機對應的曲線。這說明,在這個測試裏,性能受到TLB命中率的影響很大,若是咱們能提升TLB命中率,就能大幅度地提高性能(在後面的一個例子裏,性能提高了38%之多)。( 做者在這裏突出當元素長度>=cache line 長度的時候而且元素是隨機訪問硬件預取失效,爲何?我根據做者提供的結構體加上vtune,當結構體大小恰好爲64B的時候並無發現L1的miss
3.3.3 寫入時的行爲
爲了保持cache和內存的一致性,當cache被修改後,咱們要刷新到主存中(flush),能夠經過2種方式實現:1.write through(寫透),2.write back(寫回)
寫透是當修改cache會馬上寫入到主存,
缺點:速度慢,佔用FSB總線
優勢:實現簡單
寫回是當修改cache後不是立刻寫入到主存,而是打上已弄髒(dirty)的標記。當之後某個時間點緩存線被丟棄時,這個已弄髒標記會通知處理器把數據寫回到主存中,而不是簡單地扔掉。
優勢:速度快
缺點:當有多個處理器(或核心、超線程)訪問同一塊內存時,必須確保它們在任什麼時候候看到的都是相同的內容。若是緩存線在其中一個處理器上弄髒了(修改了,但還沒寫回主存),而第二個處理器恰好要讀取同一個內存地址,那麼這個讀操做不能去讀主存,而須要讀第一個處理器的緩存線。
3.3.4 多處理器支持
直接提供從一個處理器到另外一處理器的高速訪問,這是徹底不切實際的。從一開始,鏈接速度根本就不夠快。實際的選擇是,在其須要的狀況下,轉移到其餘處理器
如今的問題是,當該高速緩存線轉移的時候會發生什麼?這個問題回答起來至關容易:當一個處理器須要在另外一個處理器的高速緩存中讀或者寫的髒的高速緩存線的時候。但怎樣處理器怎樣肯定在另外一個處理器的緩存中的高速緩存線是髒的?假設它僅僅是由於一個高速緩存線被另外一個處理器加載將是次優的(最好的)。一般狀況下,大多數的內存訪問是隻讀的訪問和產生高速緩存線,並不髒。在高速緩存線上處理器頻繁的操做(固然,不然爲何咱們有這樣的文件呢?),也就意味着每一次寫訪問後,都要廣播關於高速緩存線的改變將變得不切實際。
人們開發除了MESI緩存一致性協議(MESI=Modified, Exclusive, Shared, Invalid,變動的、獨佔的、共享的、無效的)。協議的名稱來自協議中緩存線能夠進入的四種狀態:
  • 變動的: 本地處理器修改了緩存線。同時暗示,它是全部緩存中惟一的拷貝。
  • 獨佔的: 緩存線沒有被修改,並且沒有被裝入其它處理器緩存。
  • 共享的: 緩存線沒有被修改,但可能已被裝入其它處理器緩存。
  • 無效的: 緩存線無效,即,未被使用。
MESI協議開發了不少年,最初的版本比較簡單,可是效率也比較差。如今的版本經過以上4個狀態能夠有效地實現寫回式緩存,同時支持不一樣處理器對只讀數據的併發訪問。
(寫回如何被實現,經過監聽其處理器的狀態)
在協議中,經過處理器監聽其它處理器的活動,不需太多努力便可實現狀態變動。處理器將操做發佈在外部引腳上,使外部能夠了解處處理過程。
一開始,全部緩存線都是空的,緩存爲無效(Invalid)狀態。當有數據裝進緩存供寫入時,緩存變爲變動(Modified)狀態。若是有數據裝進緩存供讀取,那麼新狀態取決於其它處理器是否已經狀態了同一條緩存線。若是是,那麼新狀態變成共享(Shared)狀態,不然變成獨佔(Exclusive)狀態。
若是本地處理器對某條Modified緩存線進行讀寫,那麼直接使用緩存內容,狀態保持不變。若是另外一個處理器但願讀它,那麼第一個處理器將內容發給第二個處理器,而後能夠將緩存狀態置爲Shared。而發給第二個處理器的數據由內存控制器接收,並放入內存中。若是這一步沒有發生,就不能將這條線置爲Shared。若是第二個處理器但願的是寫,那麼第一個處理器將內容發給它後,將緩存置爲Invalid。這就是臭名昭著的"請求全部權(Request For Ownership,RFO)"操做。在末級緩存執行RFO操做的代價比較高。若是是寫通式緩存,還要加上將內容寫入上一層緩存或主存的時間,進一步提高了代價。
對於Shared緩存線,本地處理器的讀取操做並不須要修改狀態,並且能夠直接從緩存知足。而本地處理器的寫入操做則須要將狀態置爲Modified,並且須要將緩存線在其它處理器的全部拷貝置爲Invalid。所以,這個寫入操做須要經過RFO消息發通知其它處理器。若是第二個處理器請求讀取,無事發生。由於主存已經包含了當前數據,並且狀態已經爲Shared。若是第二個處理器須要寫入,則將緩存線置爲Invalid。不須要總線操做。
Exclusive狀態與Shared狀態很像,只有一個不一樣之處: 在Exclusive狀態時,本地寫入操做不須要在總線上聲明,由於本地的緩存是系統中惟一的拷貝。這是一個巨大的優點,因此處理器會盡可能將緩存線保留在Exclusive狀態,而不是Shared狀態。只有在信息不可用時,才退而求其次選擇shared。放棄Exclusive不會引發任何功能缺失,但會致使性能降低,由於E→M要遠遠快於S→M。
 
從以上的說明中應該已經能夠看出,在多處理器環境下,哪一步的代價比較大了。填充緩存的代價固然仍是很高,但咱們還須要留意RFO消息。一旦涉及RFO,操做就快不起來了。
RFO在兩種狀況下是必需的:
  • 線程從一個處理器遷移到另外一個處理器,須要將全部緩存線移到新處理器。
  • 某條緩存線確實須要被兩個處理器使用。{對於同一處理器的兩個核心,也有一樣的狀況,只是代價稍低。RFO消息可能會被髮送屢次。}
緩存一致性協議的消息必須發給系統中全部處理器。只有當協議肯定已經給過全部處理器響應機會以後,才能進行狀態躍遷。也就是說,協議的速度取決於最長響應時間。
對同步來講,有限的帶寬嚴重地制約着併發度。程序須要更加謹慎的設計,將不一樣處理器訪問同一塊內存的機會降到最低。
多線程測量
            圖3.19: 順序讀操做,多線程
圖3.19展現了順序讀訪問時的性能,元素爲128字節長(64位計算機,NPAD=15)。對於單線程的曲線,咱們預計是與圖3.11類似,只不過是換了一臺機器,因此實際的數字會有些小差異。
 
更重要的部分固然是多線程的環節。因爲是隻讀,不會去修改內存,不會嘗試同步。但即便不須要RFO,並且全部緩存線均可共享,性能仍然分別降低了18%(雙線程)和34%(四線程)。因爲不須要在處理器之間傳輸緩存,所以這裏的性能降低徹底由如下兩個瓶頸之一或同時引發: 一是從處理器到內存控制器的共享總線,二是從內存控制器到內存模塊的共享總線。
            圖3.20: 順序遞增,多線程
咱們用對數刻度來展現L1d範圍的結果。能夠發現,當超過一個線程後,L1d就無力了。單線程時,僅當工做集超過L1d時訪問時間纔會超過20個週期,而多線程時,即便在很小的工做集狀況下,訪問時間也達到了那個水平。
            圖3.21: 隨機的Addnextlast,多線程
最後,在圖3.21中,咱們展現了隨機訪問的Addnextlast測試的結果。這裏主要是爲了讓你們感覺一下這些巨大到爆的數字。極端狀況下,甚至用了1500個週期才處理完一個元素。若是加入更多線程,真是不可想象哪。
     圖3.22: 經過並行化實現的加速因子
圖3.22中的曲線展現了加速因子,即多線程相對於單線程所能獲取的性能加成值。測量值的精確度有限,所以咱們須要忽略比較小的那些數字。能夠看到,在L2與L3範圍內,多線程基本能夠作到線性加速,雙線程和四線程分別達到了2和4的加速因子。可是,一旦工做集的大小超出L3,曲線就崩塌了,雙線程和四線程降到了基本相同的數值(參見表3.3中第4列)。 也是部分因爲這個緣由,咱們不多看到4CPU以上的主板共享同一個內存控制器。若是須要配置更多處理器,咱們只能選擇其它的實現方式(參見第5節)。
特例: 超線程
它真正的優點在於,CPU能夠在當前運行的超線程發生延遲時,調度另外一個線程。這種延遲通常由內存訪問引發。
若是兩個線程運行在一個超線程核心上,那麼只有當兩個線程合起來的運行時間少於單線程運行時間時,效率纔會比較高
程序的執行時間能夠經過一個只有一級緩存的簡單模型來進行估算(參見[htimpact]):
  exe    = N[(1-F    mem   )T    proc    + F    mem   (G    hit     cache    + (1-G    hit   )T    miss   )]
各變量的含義以下:
N = 指令數
Fmem = N中訪問內存的比例
Ghit = 命中緩存的比例
Tproc = 每條指令所用的週期數
Tcache = 緩存命中所用的週期數
Tmiss = 緩衝未命中所用的週期數
Texe = 程序的執行時間
(也就是說在命中的時間+非命中的時間)
      圖 3.23: 最小緩存命中率-加速
紅色區域爲單線程的命中率,綠色爲雙線程,好比 若是單線程命中率不低於60%,那麼雙線程就不能低於10%。綠色區域是咱們要達到的目標,
所以,超線程只在某些狀況下才比較有用。單線程代碼的緩存命中率必須低到必定程度,從而使緩存容量變小時新的命中率仍能知足要求。只有在這種狀況下,超線程纔是有意義的。在實踐中,採用超線程可否得到更快的結果,取決於處理器可否有效地將每一個進程的等待時間與其它進程的執行時間重疊在一塊兒。並行化也須要必定的開銷,須要加到總的運行時間裏,這個開銷每每是不能忽略的。
3.3.5 其它細節
咱們已經介紹了地址的組成,即標籤、集合索引和偏移三個部分。那麼,實際會用到什麼樣的地址呢?目前,處理器通常都向進程提供虛擬地址空間,意味着咱們有兩種不一樣的地址: 虛擬地址和物理地址。
 
虛擬地址有個問題——並不惟一。隨着時間的變化,虛擬地址能夠變化,指向不一樣的物理地址。
處理器指令用的虛擬地址,並且須要在內存管理單元(MMU)的協助下將它們翻譯成物理地址。這並非一個很小的操做。在執行指令的管線(pipeline)中,物理地址只能在很後面的階段才能獲得。 這意味着,緩存邏輯須要在很短的時間裏判斷地址是否已被緩存過。而若是能夠使用虛擬地址,緩存查找操做就能夠更早地發生,一旦命中,就能夠立刻使用內存的內容。結果就是, 使用虛擬內存後,可讓管線把更多內存訪問的開銷隱藏起來
處理器的設計人員們如今使用虛擬地址來標記第一級緩存。
對於更大的緩存,包括L2和L3等,則須要以物理地址做爲標籤。由於這些緩存的時延比較大,虛擬到物理地址的映射能夠在容許的時間裏完成,並且因爲主存時延的存在,從新填充這些緩存會消耗比較長的時間,刷新的代價比較昂貴。(刷新就是寫入到內存)
 
通常來講,咱們並不須要瞭解這些緩存處理地址的細節。咱們不能更改它們,而那些可能影響性能的因素,要麼是應該避免的,要麼是伴隨更高代價的。填滿緩存是很差的行爲,緩存線都落入同一個集合,也會讓緩存早早地出問題。(和關聯性相關) 對於後一個問題,能夠經過cache address中使用虛擬地址來避免(如何避免,依靠系統?),但做爲一個用戶級程序,是不可能避免緩存物理地址的。咱們惟一能夠作的,是盡最大努力不要在同一個進程裏用多個虛擬地址映射同一個物理地址(避免同一個數據在cache中有多個記錄)。
3.4 指令緩存
其實,不光處理器使用的數據被緩存,它們執行的指令也是被緩存的。只不過,指令緩存的問題相對來講要少得多,由於:
  • 執行的代碼量取決於代碼大小。而代碼大小一般取決於問題複雜度。問題複雜度則是固定的。
  • 程序的數據處理邏輯是程序員設計的,而程序的指令倒是編譯器生成的。編譯器的做者知道如何生成優良的代碼。
  • 程序的流向比數據訪問模式更容易預測。現現在的CPU很擅長模式檢測,對預取頗有利。
  • 代碼永遠都有良好的時間局部性和空間局部性。
有一些準則是須要程序員們遵照的,但大都是關於如何使用工具的,咱們會在第6節介紹它們。而在這裏咱們只介紹一下指令緩存的技術細節。
隨着CPU的核心頻率大幅上升,緩存與核心的速度差越拉越大,CPU的處理開始管線化。也就是說,指令的執行分紅若干階段。首先,對指令進行解碼,隨後,準備參數,最後,執行它。這樣的管線能夠很長(例如,Intel的Netburst架構超過了20個階段)。在管線很長的狀況下,一旦發生延誤(即指令流中斷),須要很長時間才能恢復速度。管線延誤發生在這樣的狀況下: 下一條指令未能正確預測,或者裝載下一條指令耗時過長(例如,須要從內存讀取時)。
3.4.1 自修改的代碼
3.5 緩存未命中的因素
咱們已經看過內存訪問沒有命中緩存時,那陡然猛漲的高昂代價。可是有時候,這種狀況又是沒法避免的,所以咱們須要對真正的代價有所認識,並學習如何緩解這種局面。
3.5.1 緩存與內存帶寬 
           圖3.24: P4的帶寬
當工做集可以徹底放入L1d時,處理器的每一個週期能夠讀取完整的16字節數據,即每一個週期執行一條裝載指令(moveaps指令,每次移動16字節的數據)。測試程序並不對數據進行任何處理,只是測試讀取指令自己。當工做集增大,沒法再徹底放入L1d時,性能開始急劇降低,跌至每週期6字節。在218工做集處出現的臺階是因爲DTLB緩存耗盡,所以須要對每一個新頁施加額外處理。因爲這裏的讀取是按順序的,預取機制能夠完美地工做,而FSB能以5.3字節/週期的速度傳輸內容。但預取的數據並不進入L1d (放不進L1D放去了哪裏?)。固然,真實世界的程序永遠沒法達到以上的數字,但咱們能夠將它們看做一系列實際上的極限值
更使人驚訝的是寫操做和複製操做的性能。即便是在很小的工做集下,寫操做也始終沒法達到4字節/週期的速度。這意味着,Intel爲Netburst處理器的L1d選擇了寫通(write-through)模式,因此寫入性能受到L2速度的限制。同時,這也意味着,複製測試的性能不會比寫入測試差太多(複製測試是將某塊內存的數據拷貝到另外一塊不重疊的內存區),由於讀操做很快,能夠與寫操做實現部分重疊。最值得關注的地方是,兩個操做在工做集沒法徹底放入L2後出現了嚴重的性能滑坡,降到了0.5字節/週期!比讀操做慢了10倍! (慢在哪裏?)顯然,若是要提升程序性能,優化這兩個操做更爲重要
圖3.25採用了與圖3.24相同的刻度,以方便比較二者的差別。圖3.25中的曲線抖動更多,是因爲採用雙線程的緣故。結果正如咱們預期,因爲超線程共享着幾乎全部資源(僅除寄存器外),因此每一個超線程只能獲得一半的緩存和帶寬。因此,即便每一個線程都要花上許多時間等待內存,從而把執行時間讓給另外一個線程,也是無濟於事——由於另外一個線程也一樣須要等待。這裏偏偏展現了使用超線程時可能出現的最壞狀況。
 
寫/複製操做的性能與P4相比,也有很大差別。處理器沒有采用寫通策略,寫入的數據留在L1d中,只在必要時才逐出。這使得寫操做的速度能夠逼近16字節/週期。一旦工做集超過L1d,性能即飛速降低。因爲Core 2讀操做的性能很是好,因此二者的差值顯得特別大。當工做集超過L2時,二者的差值甚至超過20倍!但這並不表示Core 2的性能很差,相反,Core 2永遠都比Netburst強。
           圖3.27: Core 2運行雙線程時的帶寬表現
在圖3.27中,啓動雙線程,各自運行在Core 2的一個核心上。它們訪問相同的內存,但不須要完美同步。從結果上看,讀操做的性能與單線程並沒有區別,只是多了一些多線程狀況下常見的抖動。
當工做集小於L1d時,寫操做與複製操做的性能不好,就好像數據須要從內存讀取同樣。兩個線程彼此競爭着同一個內存位置,因而不得不頻頻發送RFO消息。問題的根源在於,雖然兩個核心共享着L2,但沒法以L2的速度處理RFO請求。
當工做集小於L1d時,寫操做與複製操做的性能不好,就好像數據須要從內存讀取同樣。兩個線程彼此競爭着同一個內存位置,因而不得不頻頻發送RFO消息。問題的根源在於,雖然兩個核心共享着L2,但沒法以L2的速度處理RFO請求。 而當工做集超過L1d後,性能出現了迅猛提高。這是由於,因爲L1d容量不足,因而將被修改的條目刷新到共享的L2。因爲L1d的未命中能夠由L2知足, 只有那些還沒有刷新的數據才須要RFO,因此出現了這樣的現象。這也是這些工做集狀況下速度降低一半的緣由。
 
圖3.28展現了AMD家族10h Opteron處理器的性能。這顆處理器有64kB的L1d、512kB的L2和2MB的L3,其中L3緩存由全部核心所共享。
           圖3.28: AMD家族10h Opteron的帶寬表現
你們首先應該會注意到,在L1d緩存足夠的狀況下,這個處理器每一個週期能處理兩條指令。讀操做的性能超過了32字節/週期,寫操做也達到了18.7字節/週期。可是,不久,讀操做的曲線就急速降低,跌到2.3字節/週期,很是差。處理器在這個測試中並無預取數據,或者說,沒有有效地預取數據。
另外一方面,寫操做的曲線隨幾級緩存的容量而流轉。在L1d階段達到最高性能,隨後在L2階段降低到6字節/週期,在L3階段進一步降低到2.8字節/週期,最後,在工做集超過L3後,降到0.5字節/週期。它在L1d階段超過了Core 2,在L2階段基本至關(Core 2的L2更大一些),在L3及主存階段比Core 2慢。
           圖3.29: AMD Fam 10h在雙線程時的帶寬表現
讀操做的性能沒有受到很大的影響。每一個線程的L1d和L2表現與單線程下相仿,L3的預取也依然表現不佳。兩個線程並無過渡爭搶L3。問題比較大的是寫操做的性能。兩個線程共享的全部數據都須要通過L3,而這種共享看起來卻效率不好。即便是在L3足夠容納整個工做集的狀況下,所須要的開銷仍然遠高於L3的訪問時間。再來看圖3.27,能夠發現,在必定的工做集範圍內,Core 2處理器能以共享的L2緩存的速度進行處理。而Opteron處理器只能在很小的一個範圍內實現類似的性能,並且,它僅僅只能達到L3的速度,沒法與Core 2的L2相比。
3.5.2 關鍵字加載
事實上,內存控制器能夠按不一樣順序去請求緩存線中的字。當處理器告訴它,程序須要緩存中具體某個字,即「關鍵字(critical word)」時,內存控制器就會先請求這個字。一旦請求的字抵達,雖然緩存線的剩餘部分還在傳輸中,緩存的狀態尚未達成一致,但程序已經能夠繼續運行。這種技術叫作關鍵字優先及較早重啓(Critical Word First & Early Restart)。
           圖3.30: 關鍵字位於緩存線尾時的表現
圖3.30展現了下一個測試的結果,圖中表示的是關鍵字分別在線首和線尾時的性能對比狀況。元素大小爲64字節,等於緩存線的長度。圖中的噪聲比較多,但仍然能夠看出,當工做集超過L2後,關鍵字處於線尾狀況下的性能要比線首狀況下低0.7%左右。
3.5.3 緩存設定
關於各類處理器模型的優勢,已經在它們各自的宣傳手冊裏寫得夠多了。在每一個核心的工做集互不重疊的狀況下,獨立的L2擁有必定的優點,單線程的程序能夠表現優良。考慮到目前實際環境中仍然存在大量相似的狀況,這種方法的表現並不會太差。不過,無論怎樣,咱們總會遇到工做集重疊的狀況。若是每一個緩存都保存着某些通用運行庫的經常使用部分,那麼很顯然是一種浪費。
若是像Intel的雙核處理器那樣,共享除L1外的全部緩存,則會有一個很大的優勢。若是兩個核心的工做集重疊的部分較多,那麼綜合起來的可用緩存容量會變大,從而容許容納更大的工做集而不致使性能的降低。若是二者的工做集並不重疊,那麼則是由Intel的高級智能緩存管理(Advanced Smart Cache management)發揮功用,防止其中一個核心壟斷整個緩存。
           圖3.31: 兩個進程的帶寬表現
此次,測試程序兩個進程,第一個進程不斷用SSE指令讀/寫2MB的內存數據塊,選擇2MB,是由於它正好是Core 2處理器L2緩存的一半,第二個進程則是讀/寫大小變化的內存區域,咱們把這兩個進程分別固定在處理器的兩個核心上。圖中顯示的是每一個週期讀/寫的字節數,共有4條曲線,分別表示不一樣的讀寫搭配狀況。例如,標記爲讀/寫(read/write)的曲線表明的是後臺進程進行寫操做(固定2MB工做集),而被測量進程進行讀操做(工做集從小到大)。
 
圖中最有趣的是2**20到2**23之間的部分。若是兩個核心的L2是徹底獨立的,那麼全部4種狀況下的性能降低均應發生在221到222之間,也就是L2緩存耗盡的時候。但從圖上來看,實際狀況並非這樣,特別是背景進程進行寫操做時尤其明顯。 當工做集達到1MB(220)時,性能即出現惡化,兩個進程並無共享內存,所以並不會產生RFO消息。因此,徹底是緩存逐出操做引發的問題。目前這種智能的緩存處理機制有一個問題,每一個核心能實際用到的緩存更接近1MB,而不是理論上的2MB。
3.5.4 FSB的影響
FSB在性能中扮演了核心角色。緩存數據的存取速度受制於內存通道的速度。
           圖3.32: FSB速度的影響
圖上的數字代表,當工做集大到對FSB形成壓力的程度時,高速FSB確實會帶來巨大的優點。在咱們的測試中,性能的提高達到了18.5%,接近理論上的極限。而當工做集比較小,能夠徹底歸入緩存時,FSB的做用並不大。
 
附錄1
exclusive caches:就是全部的緩存中只保留一份緩存線。
     好處:能存更多的數據。壞處:當L1 miss,L2 hit後,L2要把數據交換到L1上,這個操做比copy的花費要大
inclusive caches:上一級緩存有的必須在本緩存中出現一份備份
     好處:當刪除一個緩存線的時候只須要掃描L2的緩存線就能夠了,不須要再去掃描L1的緩存線
               若是在二級緩存很大,而且cache數據比tag還要大,tags的空間就能夠被節省下來,在L2中保留跟多的L1數據
     壞處:若是當L2的關聯性比L1差,L1的關聯性會被L2影響(不清楚情況)
               當L2的緩存線要被換出,L1的也須要被換出,可能會再次L1的miss率上升
4 虛擬內存
4.1 簡單的地址轉化
MMU會把地址映射到基於頁的方式,想cache line的地址虛擬地址也被分爲幾個部分,使用這些部分用來構建最後的物理地址
圖中前版部分指向了一個頁表,頁表中包含了物理內存頁的地址,而後再經過offset計算出頁內的偏移。這就就是一個物理地址了
頁表的被保存在內存中,OS分配一個連續的物理內存而且把基地址存入寄存器中。虛擬地址前部分被做爲頁表的index
4.2多級頁表
通常頁大小是4KB,那麼也就是2**12還有2**20,若是每一個項爲4B,那麼就須要4MB大小的頁表,很不靠譜。因此分爲了多級增長了頁表的緊湊性,而且頁保證了多個程序下對性能的影響不會太大。
4級頁表,是最複雜的4個頁表被分在不一樣的4個寄存器上,Level1是局部物理地址加上安全選項
和上面的相似,惟一不一樣的是多級頁表是分爲屢次,最後查詢到物理地址。每一個進程都有本身的頁表,因此爲了性能和可擴展性儘可能保持較小的頁表。把虛擬地址放在一塊兒,這樣能夠減小頁表空間。小的程序只會使用頁表的不多的部分。
在現實中,由於安全性的關係可執行程序的多個部分被隨機分散到各處。如果性能比安全重要,也能夠關閉隨機。
4.3 優化頁表訪問
頁表是維護在內存中的,可是須要4次訪問內存仍是很慢的,訪問cache,4次訪問也是很慢的。每一個絕對地址的計算都須要大量的訪問頁表4頁表級別的至少要12個cpu週期,而且可能會致使L1miss,致使管線斷裂。
因此並不緩存頁表,而是緩存計算後的物理地址。由於offset並不參與緩存。放翻譯結果的叫作TLB,很快可是也很小,現代的CPU都有提供多級的TLB,並使用LRU算法,當前,TLB引入了組相連,因此並非最老的就會被代替了。
Tag是虛擬地址的一部分用來訪問TLB,若是找到再加上offset就是物理地址了。在某些狀況下二級的TLB中沒有找到,就必須經過頁表計算,這個將是代價很是高的操做。在預取操做的時候爲TLB預取是不行的,(硬件預取)只能經過指令預取才有TLB預取。
4.3.1 TLB注意事項
TLB是內核全局資源全部的線程,進程都運行在同一個TLB上,CPU不能盲目的重用,由於存的是虛擬地址會出現重複的狀況。
因此有一下2個方法:
1.當頁表被修改後刷新TLB數據
2.用一個標記來識別TLB中的頁表
第一個是不行的頁表會在上下文切換的時候被天,刷新會把可能能夠繼續使用的也刷新掉了
有些cpu結構優化了,把某個區間內的刷新掉,因此代價不是很高
最優的方法是新增一個惟一識別符。可是有個問題是TLB能給的標記有限,有些標示必須可以重用,當TLB出現刷新的時候,全部的重用都要被刷新掉。
使用標記的好處是若是當前使用的被調出,下次再被調度的時候TLB任然能夠使用,出了這個還有另外2個好處:
1.內核或者VMM使用一般只須要很短的時間。沒有tag會執行2次刷新,若是有tag地址就會被保留,由於內核和vmm並不常常修改tlb,因此上次保存的任然能夠使用
2.若一個進程的2個線程進行切換,那麼就沒有必要刷新。
4.3.2 TLB的性能影響
首先就是頁的大小,頁越大減小了轉化過程,減小了在TLB的容量
可是大的頁在物理內存上必須連續,這樣會形成內存浪費。
在x86-64中2MB的頁,是有多個小的頁組成,這些小的頁在物理上市連續的。當系統運行了一段時間後,發現要分配2MB的連續內存空間變得十分困難
在linux中會在系統啓動時使用hugetlbfs文件系統預先保留,若是要增長就必須重啓系統。大的頁能夠提高性能,在資源豐富,可是安裝麻煩也不是很大的問題。好比數據庫系統。
增長最小虛擬頁的大小也是有問題,內存映射操做驗證這些頁的大小,不能讓更小的出現,若大小超過了可執行文件或者DSO的考慮範圍,就沒法加載。
第二個影響就是頁表級別減小,由於須要參與映射的位數減小,減小了TLB中的使用空間,也減小了須要移動的數據。只是對對齊的需求比較大。TLB少了性能天然變好。
4.4 虛擬化的影響
VMM只是個軟件,並不實現硬件的控制。在早期,內存和處理器以外的硬件都控制在DOM0中。如今Dom0和其餘的Dom同樣,在內存的控制上沒有什麼區別。
爲了實現虛擬化,Dom0,DomU的內核直接訪問物理內存是被限制的。而是使用頁表結構來控制Dom0,DomU上的內存使用。
無論是軟件虛擬仍是硬件虛擬,用戶域中的爲每一個進程建立的頁表和硬件虛擬,軟件虛擬是相似的。當用戶OS修改了這些頁表,VMM就會被調用,根據修改的信息,來修改VMM中的影子頁表(原本應該是硬件處理),每次修改都須要調用VMM顯然是一個代價很高的操做。
爲了減小這個代價,intel 引入了EPT,AMD引入了NPT,2個功能同樣,就是爲VMM生產虛擬的物理地址,當使用內存的時候,cpu參與把這些地址進一步翻譯爲物理地址,這樣就能夠再非虛擬話的速度來運行,更多的vmm中關於內存控制的項被刪除,減小了vmm的內存使用,vmm只須要爲每一個Dom保留一份頁表便可。
爲了更高效的使用TLB,就加入了ASID在初始化處理器的時候生產用於TLB的擴展,來避免TLB的刷新,intel引入了VPID(虛擬處理器的id)來避免TLB的刷新。
 
基於VMM的虛擬機,多有2層內存控制,vmm和os須要實現如出一轍的功能
基於KVM的虛擬機就解決了這個問題,KVM虛擬機並非和VMM同樣運行在VMM上的,而是直接運行的內核的上面,使用內核的內存管理,虛擬化被KVM VMM進程控制。儘管仍是有個2個內存控制,可是工做都是在內核中實現的,不須要再VMM再實現一遍。
虛擬化的內存訪問確定比非虛擬話代價高,並且越去解決問題,可能代價會更高,cpu視圖解決這個問題,可是隻能減弱,並不能完全解決
5. NUMA的支持
NUMA由於的特點的體系設計,因此須要OS和應用程序作特別的支持
5.1 NUMA硬件
NUMA最大的特色是CPU直接鏈接內存,這樣讓cpu訪問本地的內存代價就很低,可是訪問遠程的代價就變高。
NUMA主要解決,大內存,多CPU環境下,解決,多個CPU同時訪問內存,致使內存總線熱點,或者某個內存模塊熱點,從而下降吞吐量
AMD公司提出了一個Hypertransport的傳輸技術,讓CPU經過這個傳輸技術訪問內存,而不是直接訪問內存。
     圖5.1 立方體
這些節點的拓撲圖就是立方體,限制了節點的大小2**C,C是節點的互鏈接口個數。對於2**n個cpu立方體擁有最小的直徑(任意2點的距離)
缺點就是不能支持大量的CPU。
 
接下來就是給cpu分組,實現它們之間的內存共享問題,全部這樣的系統都須要特殊的硬件,2臺這樣的機器能夠經過共享內存,實現和工做在一臺機器上同樣。互連在NUMA中是一個很重要的因素,因此係統和應用程序必須考慮到這一點
 
5.2 OS對NUMA的支持
爲了讓NUMA儘可能使用本地的內存,這裏有個特殊的例子只會在NUMA體系結構中出現。DSO的文本段在內存中,可是全部cpu都要使用,這就覺得這基本上都要使用遠程訪問。最理想的狀態是對全部須要訪問的作一個鏡像,可是很難實現。
 
爲了不相似的狀況,應該防止常常切換到其餘節點運行,從一個cpu切換到另一個就覺得這cache內容的丟失,若是要遷移,OS會選擇一個任務的選擇一個,可是在NUMA結構體系下,選擇是有一些限制的,新的處理器內存的訪問代價必定要比老的低,若是沒有可用的處理器符合這個條件,os就沒得選擇只能切換到一個代價較高的。
 
在這個情況下有2種方向,1.只是臨時的切換,2.遷移而且把內存也遷移走(頁遷移代價過高,須要作大量的複製工做,進程也必須中止,等待數據頁遷移完畢,應該避免這種狀況的發送)
 
咱們不該該假設應用程序都使用相同大小的內存,一些程序使用大量內存,一些使用小量的,若是都是用本地,早晚本地內存會被耗盡。
 
爲了解決這個問題,有個方法就是讓內存條帶化,可是缺點就是由於遷移,致使內存訪問的開銷變大。
 
5.3 相關信息
內核發佈了一些關於處理器cache的信息(經過sysfs)
     /sys/devices/system/cpu/cpu*/cache
這裏包含了資料叫作index*,列出了CPU進程的一些信息。

  type level shared_cpu_map
cpu0 index0 Data 1 00000001
index1 Instruction 1 00000001
index2 Unified 2 00000003
cpu1 index0 Data 1 00000002
index1 Instruction 1 00000002
index2 Unified 2 00000003
cpu2 index0 Data 1 00000004
index1 Instruction 1 00000004
index2 Unified 2 0000000c
cpu3 index0 Data 1 00000008
index1 Instruction 1 00000008
index2 Unified 2 0000000c
表5.1 sysfs中雙核cpu cache信息
每一個內核都有L1i,L1D,L2。L1不和其餘cpu共享。cpu0和cpu1共享L2,cpu2和cpu3共享L2
如下是4路雙核cpu的cache信息

 

  type level shared_cpu_map
cpu0 index0 Data 1 00000001
index1 Instruction 1 00000001
index2 Unified 2 00000001
cpu1 index0 Data 1 00000002
index1 Instruction 1 00000002
index2 Unified 2 00000002
cpu2 index0 Data 1 00000004
index1 Instruction 1 00000004
index2 Unified 2 00000004
cpu3 index0 Data 1 00000008
index1 Instruction 1 00000008
index2 Unified 2 00000008
cpu4 index0 Data 1 00000010
index1 Instruction 1 00000010
index2 Unified 2 00000010
cpu5 index0 Data 1 00000020
index1 Instruction 1 00000020
index2 Unified 2 00000020
cpu6 index0 Data 1 00000040
index1 Instruction 1 00000040
index2 Unified 2 00000040
cpu7 index0 Data 1 00000080
index1 Instruction 1 00000080
index2 Unified 2 00000080
     圖5.2 Opteron CPU cache信息
和圖5.1相似,可是看錶就會發現L2不和任何cpu共享
 
 /sys/devices/system/cpu/cpu*/topology顯示了sysfs關於CPU拓撲信息

  physical_
package_id
core_id core_
siblings
thread_
siblings
cpu0 0 0 00000003 00000001
cpu1 1 00000003 00000002
cpu2 1 0 0000000c 00000004
cpu3 1 0000000c 00000008
cpu4 2 0 00000030 00000010
cpu5 1 00000030 00000020
cpu6 3 0 000000c0 00000040
cpu7 1 000000c0 00000080
     圖5.3 Opteron CPU拓撲信息
由於thread_sinlings都是設置了一個bit,因此沒有超線程,是一個4個cpu每一個cpu有2個內核。
 
/sys/devices/system/node這個目錄下包含了系統中NUMA的信息,表5.4顯示了總要的信息

  cpumap distance
node0 00000003 10 20 20 20
node1 0000000c 20 10 20 20
node2 00000030 20 20 10 20
node3 000000c0 20 20 20 10
     圖5.4 sysfs Opteron 節點信息
上圖顯示了,cpu只有4個,有4個節點,distance表示了,他們訪問內存的花費(這個是不正確的,cpu中至少有一個是鏈接到南橋的,因此花費可能不止20.)
 
5.4 遠程訪問開銷
     圖5.3 多個節點的讀寫性能
讀性能優於寫,這個能夠預料,2個1hop性能有一點小差異,這個不是關鍵,關鍵是2hop性能大概比0hop低了30%到49%,寫的性能2-hop比0-hop少了32%,比1-hop少了17%,下一代AMD處理器hypertransport將是4個,對於4路的cpu,間距就是1。可是8路的cpu仍是有相同的問題,由於立方體中8節點的間距仍是3.
 
最後一部分信息是來自PID文件的
00400000 default file=/bin/cat mapped=3 N3=3
00504000 default file=/bin/cat anon=1 dirty=1 mapped=2 N3=2
00506000 default heap anon=3 dirty=3 active=0 N3=3
38a9000000 default file=/lib64/ld-2.4.so mapped=22 mapmax=47 N1=22
38a9119000 default file=/lib64/ld-2.4.so anon=1 dirty=1 N3=1
38a911a000 default file=/lib64/ld-2.4.so anon=1 dirty=1 N3=1
38a9200000 default file=/lib64/libc-2.4.so mapped=53 mapmax=52 N1=51 N2=2
38a933f000 default file=/lib64/libc-2.4.so
38a943f000 default file=/lib64/libc-2.4.so anon=1 dirty=1 mapped=3 mapmax=32 N1=2 N3=1
38a9443000 default file=/lib64/libc-2.4.so anon=1 dirty=1 N3=1
38a9444000 default anon=4 dirty=4 active=0 N3=4
2b2bbcdce000 default anon=1 dirty=1 N3=1
2b2bbcde4000 default anon=2 dirty=2 N3=2
2b2bbcde6000 default file=/usr/lib/locale/locale-archive mapped=11 mapmax=8 N0=11
7fffedcc7000 default stack anon=2 dirty=2 N3=2
N0-N3表示了不一樣的節點,後面表示在各個節點開闢的內存頁數
從圖5.3中,咱們發現讀性能降低了9%-30%,若是L2失效,要用遠程的內存,那麼將會增長9%-30%的開銷。
     圖5.4遠程內存的操做
這裏讀取性能下降了20%,可是前面圖5.3中是9%,差距怎麼大,想是使用老的cpu(這些圖來自AMD的amdccnuma文檔,只有AMD知道怎麼回事兒了)( 我特地去查了amdccnuma並無以上2個圖
寫和複製操做也是20%,當工做集超過cache時,讀寫操做就不比本地的慢了,主要緣由是訪問主存的開銷( 不明白,感受沒有講清楚
6 程序員應該作什麼
6.1 繞過Cache line
對於並非立刻要使用的數據,先讀取在修改並不利於性能,由於這樣會讓cache效果變低。好比矩陣寫入最後一個的時候第一個經常由於不太使用被犧牲掉了。
因此有了非臨時寫入,直接把數據寫入到內存。由於處理器會使用write-combining,使得寫入開銷並非很大。
#include <emmintrin.h>
void setbytes(char *p, int c)
{
  __m128i i = _mm_set_epi8(c, c, c, c,
                           c, c, c, c,
                           c, c, c, c,
                           c, c, c, c);
  _mm_stream_si128((__m128i *)&p[0], i);
  _mm_stream_si128((__m128i *)&p[16], i);
  _mm_stream_si128((__m128i *)&p[32], i);
  _mm_stream_si128((__m128i *)&p[48], i);
}
由於寫入綁定寫入只在最後一條指令發送,直接寫入不但避免了先讀後修改,也避免了污染cache
下面測試矩陣訪問的2中方式:行訪問,列訪問
主要的區別是行訪問是順序的,可是列訪問時隨機的。
圖 6-1矩陣訪問模式


Inner Loop Increment
  Row Column
Normal 0.048s 0.127s
Non-Temporal 0.048s 0.160s
直接寫入內存和手動寫入幾乎同樣快,主要緣由是使用了write-combining,還有就是寫入並不在意順序,這樣處理器就能夠直接寫回,儘量的使用帶寬。
列訪問方式,並無write-combining,內存單元必須一個一個寫入,由於是隨機的。當在內存芯片上寫入新行也須要選擇這一行,有相關的延遲。
 
在讀入方面尚未非零食訪問的預取,須要經過指令顯示預取,intel實現了NTA load,實現一個buffer load,每一個buffer大小爲一個cache line。
如今cpu對非cache數據,順序訪問訪問作了很好的優化,也能夠經過cache,對內存的隨機訪問下降開銷。
 
6.2 Cache Access
6.2.1 優化L1D訪問
經過demo的修改,來提升L1D的訪問,demo實現如下功能:
代碼:
for (i = 0; i < N; ++i)
    for (j = 0; j < N; ++j)
      for (k = 0; k < N; ++k)
        res[i][j] += mul1[i][k] * mul2[k][j];
從代碼中看到mul2的訪問方式是列的方式,基本上都是隨機的,那麼咱們能夠先轉化如下再計算:
double tmp[N][N];
  for (i = 0; i < N; ++i)
    for (j = 0; j < N; ++j)
      tmp[i][j] = mul2[j][i];
  for (i = 0; i < N; ++i)
    for (j = 0; j < N; ++j)
      for (k = 0; k < N; ++k)
        res[i][j] += mul1[i][k] * tmp[j][k];
測試結果:
                        Original          Transposed
Cycles 16,765,297,870 3,922,373,010
Relative 100% 23.4%
轉化後,性能高了76.6%,主要是非順序訪問引發。
爲了和cache line對齊,能夠再對代碼修改:
#define SM (CLS / sizeof (double))

  for (i = 0; i < N; i += SM)
      for (j = 0; j < N; j += SM)
          for (k = 0; k < N; k += SM)
              for (i2 = 0, rres = &res[i][j],
                   rmul1 = &mul1[i][k]; i2 < SM;
                   ++i2, rres += N, rmul1 += N)
                  for (k2 = 0, rmul2 = &mul2[k][j];
                       k2 < SM; ++k2, rmul2 += N)
                      for (j2 = 0; j2 < SM; ++j2)
                          rres[j2] += rmul1[k2] * rmul2[j2];
性能以下:
  Original Transposed Sub-Matrix Vectorized
Cycles 16,765,297,870 3,922,373,010 2,895,041,480 1,588,711,750
Relative 100% 23.4% 17.3% 9.47%
和轉化比性能有了6.1%的提高,最後一個是向量結果
代碼以下:
#include <stdlib.h>
#include <stdio.h>
#include <emmintrin.h>
#define N 1000
double res[N][N] __attribute__ ((aligned (64)));
double mul1[N][N] __attribute__ ((aligned (64)));
double mul2[N][N] __attribute__ ((aligned (64)));
#define SM (CLS / sizeof (double))

int main (void) { // ... Initialize mul1 and mul2
int i, i2, j, j2, k, k2; double *restrict rres; double *restrict rmul1; double *restrict rmul2; for (i = 0; i < N; i += SM) for (j = 0; j < N; j += SM) for (k = 0; k < N; k += SM) for (i2 = 0, rres = &res[i][j], rmul1 = &mul1[i][k]; i2 < SM; ++i2, rres += N, rmul1 += N) { _mm_prefetch (&rmul1[8], _MM_HINT_NTA); for (k2 = 0, rmul2 = &mul2[k][j]; k2 < SM; ++k2, rmul2 += N) { __m128d m1d = _mm_load_sd (&rmul1[k2]); m1d = _mm_unpacklo_pd (m1d, m1d); for (j2 = 0; j2 < SM; j2 += 2) { __m128d m2 = _mm_load_pd (&rmul2[j2]); __m128d r2 = _mm_load_pd (&rres[j2]); _mm_store_pd (&rres[j2], _mm_add_pd (_mm_mul_pd (m2, m1d), r2)); } } }
// ... use res matrix
return 0; }
 
對於寫入大量數據,可是同時使用到不多數據的狀況是很常見的如圖就是這種狀況:
 
順序訪問和隨機的訪問在L2緩存以前都很容易理解,可是超過L2以後,回到了10%不是由於開銷變小,是由於內存訪問開銷太大,已經不成比例了(有點無法理解,爲何會不成比例)。
 
軟件pahole能夠從代碼級分析這個結構體可能會用幾個cacheline
struct foo {
  int a;
  long fill[7];
  int b;
};
在編譯的時候須要帶上調試信息才行,我是在linux 下使用。
struct foo {
        int                        a;                    /*     0     4 */

        /* XXX 4 bytes hole, try to pack */

        long int                   fill[7];              /*     8    56 */
        /* --- cacheline 1 boundary (64 bytes) --- */
        int                        b;                    /*    64     4 */
}; /* size: 72, cachelines: 2 */
   /* sum members: 64, holes: 1, sum holes: 4 */
   /* padding: 4 */
   /* last cacheline: 8 bytes */
上面的信息清晰,這個結構體須要2個cache line,大小爲72字節,成員大小64字節,空的字節:4個,有1個空洞
元素b也是4個字節,做者這裏說由於int 4個字節要和long 對齊因此被浪費了有個4字節是爲了和fill對齊。可是我本身在測試的時候沒有發現這種狀況,在虛擬機上測試的。
使用pahole能夠輕易的對元素重排,而且能夠確認,那些元素在一個cache line上。結構體中的元素順序很重要,因此開發人員須要遵照如下2個規則:
1.把多是關鍵字的放到結構體的頭部
2.若沒有特定的順序,就以結構體元素的順序訪問元素
 
對於小的結構體能夠隨意的排列結構體順序,可是對於大的結構體須要有一些規則
若是自己不是對齊的結構體,那麼從新排序沒有什麼太大的價值,對於結構化的類型,結構體中最大的元素決定告終構體的對齊。即便結構體小於cache line,可是對齊後可能就比cache line 大。有2個方法確保結構體設置的時候對齊。
1.顯示的對齊請求,動態的malloc分配,一般要那些和對齊匹配的一般是標準的類型(如long,double)。也能夠使用posix_memalign作更高級別的對齊。如果編譯器分配的,變量能夠使用變量屬性struct strtype variable  __attribute((aligned(64)));這樣這個變量以64爲字節對齊
使用posix_memalign會致使碎片產生,並可能讓內存浪費。使用變量屬性對齊不能使用在數組中,除非數組元素都是對齊的。
2.對於類型的對齊能夠使用類型屬性
struct strtype {
    ...members...
  } __attribute((aligned(64)));
這樣設置以後全部編譯器分配的對象都是對齊的,包括數組,可是用malloc分配的不是對齊的,仍是posix_memalign對齊,能夠使用gcc的alignof傳入用於第二個參數。
 
x86,x86-64處理器支持對非對齊訪問可是比較慢,對於RISC來講對齊並非什麼新鮮事,RISC指令自己都是對齊的,有些結構體系支持非對齊的訪問,可是速度老是比對齊的慢。
 
如圖是對齊訪問和非對齊訪問的對比,有意思的地方是當work set超過2mb的時候開始下滑,是由於,內存訪問變多,而且內存訪問的時間佔的比重大。
關於對齊必須認真對待,居然支持非對齊訪問,可是性能也不能可能比對齊好。
 
對齊也是有反作用的,如使用了自動變量對齊,那麼就須要在全部用到的地方都要對齊,可是編譯器沒法控制調用和堆棧,因此有2個解決方法。
1.代碼和堆棧對齊,有必要的話就填充空白.2.全部的調用都要對齊。
對於調用對齊,若是被調用要求對齊,可是調用者不對其的話就會出錯。
對於和堆棧對齊,堆棧幀不必是對齊大小的整數倍,因此要填充空白,編譯器是知道堆棧幀大小的,因此能夠處理。
 
當要對VLA(經過變量決定數組長度),或者alloca的變量對齊,大小隻能在運行的時候知道,因此對他們作對齊可能會照成代碼變慢,由於生產了其餘代碼。
gcc提供了一個選項能夠更靈活的設置stack的對齊, -mpreferred-stack-boundary =N,這個N表示對齊爲2的N次。
在編譯x86程序的時候設置這個參數爲2,能夠減小stack的使用並提升代碼執行速度。
對於86-64,調用ABI,浮點類型,是經過SSE寄存器和SSE指令須要16個字節對齊。
 
對齊只是優化性能的一個方面
對於比較大的workset,從新整理數據結構是頗有必要的。不是把全部的元素都放在一個結構體中,而是更具經常使用程度,進程組織。
若數據集中在同一個cache set中會致使conflict misses。
圖中:x表示list中2個元素的距離,y表示list總長度,z表示運行時間
當list的全部元素都在一個cache set的時候,元素個數高於關聯度時,訪問就要從L2中讀取增長了讀取時間,這就是圖中顯示的特色。(能猜想數據時均勻的分佈在L1每一個cache set中,能力有限未能重現conflict miss)。非對齊的訪問或增長cache line 的conflict miss。
 
總結:對於data cache訪問主要的優化方式是1.順序訪問(提升預取性能)2.對齊(和cache line,減小cache讀取次數) 3.減小結構體對cache的佔用(也是爲了減小cache讀取) 4.減小conflict miss
 
6.2.2 優化L1指令cache訪問
由於經過編譯器處理,因此程序員並不能直接的控制代碼,線性讓處理器能夠高效獲取指令,可是若是有jump會打破這個線性,由於:1.jump是動態的,2.若是出現miss就會花費很長的時間恢復(由於pipeline)。
主要問題仍是在分支預測上的成功率,分支預測在運行到這個指令以前會先把指令取來,若是預測錯誤,就會花費比較大的時間。
 
1.減小代碼大小,注意loop unrolling 和inlining的代碼量。
     內聯對代碼分支預測有好處,可是若是內聯代碼過大,或者被對此調用反而會增長code的大小。
2.儘可能線性執行,讓pipleline不會出現斷的狀況
     碰到跳轉,要不太執行的塊移動到外面,在gcc 中能夠使用,
     
      #define unlikely(expr) __builtin_expect(!!(expr), 0)
      #define likely(expr) __builtin_expect(!!(expr), 1)
      而且在編譯的時候使用 -freorder-blocks對代碼進行重排,還有另外一選項 -freorder-blocks-and-partition 可是有使用限制,其不能和exception handling 一塊兒使用
 
     inter core2對於小的順序引入了一個基數LSD(loop stream detector),若是循環指令不超過18個,這個LSD,須要4個decoder,最多4個跳轉指令,執行64次以上,
     那麼  就會被鎖定在指令隊列中,下次運行就會變得很快。
3.若是有意義就使用代碼對齊
     對齊的意義在於可以造成順序的指令流,對齊可讓預取更加有效。也就意味着讓decode更加有效。若出現miss,那麼pipeline就會斷開。
     從如下幾點對齊代碼比較有效:1.函數的開始,2.基本代碼塊的開始,3.循環的開始。
     在第一點和第二點的優化中,編譯器每每經過插入nop指令,來對齊,因此有一些開銷。
     對於第三點,有點不一樣的是,在循環以前每每是其餘的順序的代碼,若是,其餘代碼和循環之間有空隙,那麼必須使用nop或者跳轉到循環的開始,若循環有不常執行,
     那麼nop和jump就會很浪費,比非對齊的開銷還要多。
     -falign-functions = N,對函數的對齊,對齊2的N次
     -falign-jumps = N ,對跳轉的對齊
     -falign-loops = N對循環的對齊
 
6.2.3 優化L2的cache訪問
     L1的優化和L2差很少,可是L2的特色是:1.若是miss了,代價很高,由於可能要訪問內存了。2.L2cache一般是內核共享或者超線程共享的。
     因爲l2cache 差別較大,而且,並非都引用於數據的。因此須要一個動態的計算每一個線程或者內核的最低安全限制,
     cache結構在Linux下可在 /sys/devices/system/cpu/cpu*/cache。
 
6.2.4 優化TLB使用
     要有話TLB,1.減小內存使用,2.減小查找代價,減小被分配的頁表的個數。
     ASLR會致使頁表過多,由於ASLR會讓stack,DSO,heap隨機在執行的是否分配。爲了性能能夠把ASLR關掉,可是少了安全性。
     還有一種方法是mmap的MAP_FIXED選項來分配內存地址,可是十分危險,只有在開發指導最後一級頁表的邊界,並可以選擇合適地址的前提下使用。
 
6.3預取
6.3.1 硬件預取
     只有2次以上的miss纔會除非預取,單個的miss觸發預取會照成性能問題,隨機的內存訪問預取會對fsb照成不必的浪費。
     L2或更高的預取能夠和其餘內核或者超線程共享,prefetch不能越過一個頁 由於硬件預取不懂語義,可能引發page fault或者fetch一個並不須要的頁。
     在不須要的時候引發prefetch 須要調整程序結構才能解決 在指令中插入未定義指令是一種解決方法,體系結構提供所有或者部分關閉prefetch
6.3.2 軟件預取
     硬件預取的好處是:不須要軟件處理,缺點:預取不能超過1頁。而軟件預取須要源代碼配合。
#include <xmmintrin.h>
enum _mm_hint
{
  _MM_HINT_T0 = 3,
  _MM_HINT_T1 = 2,
  _MM_HINT_T2 = 1,
  _MM_HINT_NTA = 0
};
void _mm_prefetch(void *p, enum _mm_hint h);     
以上的命令會把指針的內容預取到cache中。若是沒有可用空間了,那麼會犧牲掉其餘數據,沒不要的預取應該要避免,否則反而會增長帶寬的消耗。
_MM_HINT_T0,_MM_HINT_T1,_MM_HINT_T2,_MM_HINT_NTA,分別預取到L1,L2,L3, _MM_HINT_NTA的NTA是non-temporal access的縮寫,告訴處理器儘可能避免訪問這個數據,由於只是短期使用,因此處理器若是是包含性的就不讀取到低級的cache中,若是從L1d中犧牲,不須要推到L2中,而是直接寫入內存。要當心使用,若是work set太大,犧牲了不少cache line,這些cache line 由要從內存中加載,有些得不償失了。
圖中使用NPAD=31 每一個元素大小爲128,只有8%的提高,很不明顯。加入預取代碼效果不是很明顯,能夠使用性能計數器,在須要的位子上面加預取。
-fprefetch-loop-arrays是GCC提供的編譯選項 其將爲優化數組循環插入prefetch指令,對於小的循環就沒有好處,要當心使用
 
6.3.3 特殊類型的預取:推測(speculation)
主要的用處是在其餘不相關的代碼中映入預取,來hide延遲
 
6.3.4 幫助thread
把預取放在代碼中會讓邏輯變得很複雜,能夠使用其餘處理器來作幫助thread,幫忙預取數據。難度是不能太慢,也不能太快,增長L1i的有效性。
1.使用超線程,能夠預取到l2,升至l1
2.使用dumber線程,只作簡單的讀取和預取操做
上圖看到性能有提高。
能夠經過libNUMA中的函數,來知道cache的共享信息
#include <libNUMA.h>
ssize_t NUMA_cpu_level_mask(size_t destsize,
                            cpu_set_t *dest,
                            size_t srcsize,
                            const cpu_set_t*src,
                            unsigned int level);
 
cpu_set_t self;
  NUMA_cpu_self_current_mask(sizeof(self),
                             &self);
  cpu_set_t hts;
  NUMA_cpu_level_mask(sizeof(hts), &hts,
                      sizeof(self), &self, 1);
  CPU_XOR(&hts, &hts, &self);
self 是當前的內核,hts是能夠被用來作help線程的內核。
 
6.3.5直接cache訪問
現代的硬件能夠直接寫入到內存,可是對於立刻要使用的數據,就會出現miss,因此intel有了一個技術,能夠直接報包的數據寫入到cache中。使用DCA(direct cache access)來標記這個包,寫在包頭中,如圖:
       
左右沒有使用DCA標記,右圖有DCA標記, DCA標記若是fsb識別那麼就直接傳入到L1cache中了。這個功能須要全部硬件都有相關功能才能使用。

6.4多線程優化
關於多線程的優化,主要從如下3方面展開
1.併發
2.原子性
3.帶寬
 
6.4.1 併發優化
一般cache的優化是那數據儘可能放在一塊兒,減小代碼footprint,可以最大化的把內存放到cache中,可是多線程每每訪問類似的數據,這樣會致使,若是一個線程對一個內存進行修改請求,cache line 必須是 獨佔(E)狀態,這就意味着要作RFO。就算全部的線程使用獨立的內存,而且是獨立的仍是可能會出現這種情況。
上圖測試在4個P4處理器上進行,在第9.3中有這個測試的demo,代碼主要是作對一個內存作5000W次的自增,藍色的值表示,線程在分開的cache上自增,紅色的表示在同一個cache上自增。
當線程寫入一個cache是,全部的其餘cache都會被延遲,而且cache是獨立的。形成紅色部分比藍色部分高的緣由。
上圖是intel core2 QX6700 上的測試,4核有2個獨立的L2,並無出現這個問題,緣由就是cache是共享的。
 
簡單的解決問題的方法:把變量放到線程各自的cache line中,線程變量。
1.把讀寫的變量和只讀的變量發開存放。也能夠用來解決讀多寫少的變量。
2.會被同時使用的變量放到一個結構體上,用結構體來保證,變量在比較近的。
3.把被不一樣線程讀寫的變量寫入到他們本身的cache line中。
     int foo = 1;
  int baz = 3;
  struct {
    struct al1 {
      int bar;
      int xyzzy;
    };
    char pad[CLSIZE - sizeof(struct al1)];
  } rwstruct __attribute__((aligned(CLSIZE))) =
    { { .bar = 2, .xyzzy = 4 } };
4.把被多線程使用,可是使用都是獨立的變量放入各自線程中,如:
      int foo = 1;
  __thread int bar = 2;
  int baz = 3; 
  __thread int xyzzy = 4;   
6.4.2 原子優化
做者只介紹了原子操做的相關操做,並說盡可能減小原子操做。
 
6.4.3 帶寬衝突
每一個處理器都有與內存鏈接的帶寬,因爲體系結構的不一樣,多個處理器可能共享一個總線或者北橋。那麼就可能會出現帶寬的爭用問題。
1.買速度快的服務器,若是重寫程序比較昂貴的狀況下
2.優化程序避免miss,調度器並不清楚workload,只能收集到cache miss,不少狀況下並無什麼幫助
3.儘可能讓cache存儲變得更有效,讓處理器處理一塊數據。
     
     
     cache使用效率低下
     
     cache 使用效率高
     調度器不知道workload,因此須要開發人員本身設置調度。
     
進程調度:     
#define _GNU_SOURCE
#include <sched.h>

int sched_setaffinity(pid_t pid, size_t size, const cpu_set_t *cpuset);
int sched_getaffinity(pid_t pid, size_t size, cpu_set_t *cpuset);
線程的調度:
 
  
#define _GNU_SOURCE
#include <pthread.h>

int pthread_setaffinity_np(pthread_t th, size_t size,
                           const cpu_set_t *cpuset);
int pthread_getaffinity_np(pthread_t th, size_t size, cpu_set_t *cpuset);
int pthread_attr_setaffinity_np(pthread_attr_t *at,
                                size_t size, const cpu_set_t *cpuset);
int pthread_attr_getaffinity_np(pthread_attr_t *at, size_t size, 
                                cpu_set_t *cpuset);
6.5 NUMA編程
NUMA的特色是:cpu內訪內存時,節點訪問代價高,非跨節點代價低。
2個線程在同一個內核中使用相同的cache協做比使用獨立的cache要快。
6.5.1 內存策略
對於numa來講內存的訪問,尤其重要,全部出了個內存策略也就是內存分配策略,具體說就是在哪裏分配內存。
linux 內核支持的策略種類:
1.MPOL_BIND:在給定的node中分配,若是不行則失敗
2.MPOL_PREFERRED:首選在給定的節點分配,失敗考慮其餘節點
3.MPOL_INTERLEAVE:經過虛擬地址區域,決定在哪一個節點上分配
4.MPOL_DUFAULT:在default區域上分配
以上的遞歸的方法定義策略只是對了一半,其實,內存策略的有個一個框圖:
若地址落在VMA上那麼,使用VMA策略,若在共享上,那麼使用ShMem Policy,若是,這個地址沒有指定策略,那麼使用task policy,若是定義了task policy那麼就task policy處理,若是沒有就system default policy處理。
 
 
6.5.2 指定策略
#include <numaif.h>

long set_mempolicy(int mode, 
                   unsigned long *nodemask, 
                   unsigned long maxnode);
經過以上方法來設置task policy
6.5.3 交換和策略
交換後,內存中的節點信息就會消失,對於一個多處理器共享的頁來講隨着時間的推移,相關性就會被改變,
6.5.4 VMA Policy
經過函數來裝載 VMA Policy
#include <numaif.h>

long mbind(void *start, unsigned long len,
           int mode,
           unsigned long *nodemask,
           unsigned long maxnode,
           unsigned flags);
VMA區域是 start,start+len
mode 就是前面提到的4個策略
flag參數:
MPOL_MF_STRICT:若是mbind中的地址不在nodemask中,就報錯
MPOL_MF_MOVE:若是有地址不在這個node上,試圖移動。
MPOL_MF_MOVEALL:會移動全部,不僅僅是這個進程用到的頁表,這個參數會影響其餘進程的訪問
對於一個已經保留了地址,可是沒有分配的地址空間,能夠使用mbind可是不帶參數。
6.5.5 查詢節點信息
獲取numa策略
#include <numaif.h>
long get_mempolicy(int *policy,
             const unsigned long *nmask,
             unsigned long maxnode,
             void *addr, int flags);
經過虛擬地址查詢節點信息
#include <libNUMA.h>

int NUMA_mem_get_node_idx(void *addr);
int NUMA_mem_get_node_mask(void *addr,
                           size_t size,
                           size_t __destsize,
                           memnode_set_t *dest);
查詢cpu對應的節點
#include <libNUMA.h>

int NUMA_cpu_to_memnode(size_t cpusetsize,
                        const cpu_set_t *cpuset,
                        size_t memnodesize,
                        memnode_set_t *
                        memnodeset);
#include <libNUMA.h>

int NUMA_memnode_to_cpu(size_t memnodesize,
                        const memnode_set_t *
                        memnodeset,
                        size_t cpusetsize,
                        cpu_set_t *cpuset);
6.5.6 cpu和節點設置
使用cpuset來限制用戶或者程序對cpu和內存的使用。
6.5.7 顯示優化NUMA
當內地內存和affinity規則,若是全部的線程要訪問同一個地址,那麼訪問本地內存,和affinity是沒有效果的。
對於只讀的內存能夠直接使用複製,把數據複製到要用的節點。
可是對於可讀寫的就比較麻煩,若是計算不依賴於上次的結果,能夠把各個節點的計算累計到各個node上,而後再寫入內存。
若是訪問內存是固定的,遷移到本地node中,若是訪問很高,可是非本地訪問減小,那麼就是有用的。可是要注意,頁遷移仍是有不小的開銷的。
6.5.8  利用全部 帶寬
在圖5.4中,訪問遠程和本地並無明顯的差距,這個是由於,把寫入再也不使用的數據,經過內存附加,寫入到了其餘節點上去了,由於帶寬鏈接DRAM和cpu之間的內部鏈接是同樣的,因此,看不出來性能的差距。
利用全部的帶寬是不少方面的:一個就是肯定cache是無效的,由於須要經過遠程來放置,另一個是遠程內存是否須要帶寬
相關文章
相關標籤/搜索