1. 程式人生 > >深入理解計算機系統----第六章儲存器層次結構

深入理解計算機系統----第六章儲存器層次結構

 

原文部落格地址:https://www.jianshu.com/p/88c889e4fef3

目 錄

在本章中,我們會先了解儲存技術(SRAM\DRAM\ROM\旋轉固態硬碟),描述這些儲存器是如何被組織成層次結構的。接下來會談到什麼是擁有良好區域性性的程式以及編寫這樣的程式需要注意的問題。然後我們開始探究本質,為什麼說擁有良好區域性性的程式會執行的更快。就要求我們要學習快取記憶體,並教會大家理解程式的區域性性的真正意義,使得你自己不僅僅遵守規則,而是瞭解其內部原理獲取更大的自由。

1.1 儲存技術


① 隨機訪問儲存器

靜態RAM:(SRAM)用作快取記憶體,通常只有幾兆,在CPU晶片上、下;硬體設計中,將每個位存在一個雙穩定的儲存單元中,如下圖所示,只有在兩邊的時候保持穩定性:

只要有電,會永遠保持它的值,不懼干擾

動態RAM:(DRAM)用作主存(我們通常說的機器的記憶體),通常幾百、幾千兆。每個單位使用一個電容和一個訪問電晶體構成,容易被幹擾,有的加入有糾錯碼。系統需要週期性讀出,然後重新整理重寫儲存器的每一位。

DRAM詳細構造圖:

一個128位的16X8的DRAM構造檢視

DRAM晶片被分割成16個超單元,每個超單元又由w個DRAM單元組成,一個16*w的DRAM共儲存16w位資訊。超單元被組織成一個4*4的陣列,圖中的超單元地址用(2,1)表示。資訊通過引腳流入和流出晶片,每個引腳攜帶1位訊號,圖中有兩組引腳,其中data引腳為8個,能傳出或接受來自晶片的一個位元組資料,還有兩個addr引腳,攜帶2位的行列超單元地址。

訪問示例(我們來看看是如何訪問超單元(2,1)處的內容)

分析:我們首先來思考一個問題,為什麼要組成一個二維的陣列,而不是一位陣列,這樣訪問的速度就快很多啊。以上圖為例,我們如果要建造一個128位的DRAM,我們用一維陣列實現的話,我們需要提供大量的地址引腳來提供訪問(硬體設計上太費了)。

讀取一個超單元

為了加快二維陣列的訪問,儲存控制器在讀取(2,1)處的內容的時候,使用addr先發送行地址2到DRAM晶片中,拷貝整個第二行的內容到內部緩衝區中,然後傳送列地址1,從內部行緩衝區中讀取1的地址內容通過data傳送到儲存控制器中去。

② 儲存器模組

讀一個儲存器模組的內容

如圖所示是一個64M的主存,晶片編號0-7,每個晶片儲存8M的資料,儲存器模組將其組合起來,聚合記憶體。將每單個晶片的超單元對映成主存地址A的各個欄位。這樣控制器收到一個主存地址A的時候,儲存控制器將其選擇包含的具體晶片,將A轉換成(i,j)的形式,然後將(i,j)傳送到晶片模組中開始取資料。

備註:儲存在ROM裝置中的程式通常稱為韌體,當一個計算機系統通電以後,它會執行儲存在ROM中的韌體。

③ 訪問主存(讀事務、寫事務)

資料流通過匯流排,在CPU和DRAM中傳遞資料,匯流排能攜帶:地址、資料和控制訊號。

CPU和主存的匯流排構照圖

讀事務:考慮當我們執行,movl A,%eax的情況,地址A的內容會被載入到eax中去,匯流排發起讀事務(分三步):①CPU將A的地址匯流排放到系統總線上,橋作為中轉點,將地址訊號傳送到儲存器總線上去;②主存感覺到了儲存器總線上的地址訊號,從儲存器總線上讀地址,並從主存中取出相應的資料,寫入到儲存器總線上去,橋將資料專遞到系統中線中去;③CPU感覺到了系統總線上的資料,將資料拷貝到eax中。

I/O橋作為中轉,將地址訊號從系統匯流排轉到儲存器匯流排,然後又將資料從儲存器匯流排轉到系統匯流排。在這個過程中,CPU始終是從系統總線上傳送地址,讀取資料,主存始終是從儲存器總線上接受地址併發送資料。(寫事務是一個逆向過程不做講解)

④ 磁碟儲存 (硬碟)

構造:

磁碟由碟片構成,表面覆蓋的有磁性材料,中間是一個主軸,通過旋轉讀取和記錄資料。每組同心圓磁軌分割的區域就是一個扇區。扇區之間是有間隙的,如圖:

磁碟構造

磁碟讀寫操作:

磁碟動態特性

磁碟以扇區為單位來讀寫資料,對扇區的訪問時間由三個部分組成:尋道之間、旋轉時間、傳送時間。以圖a為例,當我們要訪問同心圓磁軌5的內容時,尋道時間是指傳動手臂將讀寫頭移動到同心圓第五磁軌的時間,旋轉時間指的是同心圓5開始讀取內容的位置,如果手臂移動到第五磁軌的時候讀寫位置剛過,就要等磁碟旋轉一圈之後再讀取;傳送時間,扇區第一個位處於讀寫頭的時候,讀寫該扇區的時間。(尋道時間和旋轉延遲大致相當)

磁碟為什麼都是密封的?在傳動臂末端的讀/寫頭在磁碟表面高度大約0.!微米處的一層薄薄的氣墊上飛翔(就是字畫上這個意思),速度大約為80km/h。這可以比喻成將Sears Tower(譯者注,一座位於芝加哥的108層和442米高的摩天大樓)放倒,然後讓它在距離地面2.5 cm(1英寸)的高度上飛行環繞地球,境地球一天只需要8秒鐘!在這樣小的間隙裡,盤面上--粒微小的灰塵都像一塊巨石。如果讀/寫頭碰到了這樣的一塊巨石,讀/零頭會停下來,撞到盤面——所謂的讀/寫頭衝撞(head crash)。

邏輯磁碟塊:

以我們正在使用的計算機為例,當我們安裝的有ghost軟體開始備份的時候,就會要求選擇備份檔案的位置,我們看到的是1.1-1.6左右的可以選擇的硬碟,實際上磁碟雖然進行了分割槽,但本質上仍然只有一塊磁碟。在磁碟中有一個小韌體,磁碟控制器,維護著磁碟扇區之間的對映關係。假設我們要開啟E:上的一個檔案,控制器就會執行一個快速表查詢,將該處的內容翻譯成(盤面、磁軌、扇區),等到傳動臂移動到正確的位置時,將內容讀到一個緩衝區,然後拷貝的主存中去。

邏輯塊的作用:當我們對磁碟進行分割槽以前都要求我們進行格式化,這樣做是讓磁碟控制器,讀取磁碟的基礎內容,同時建立備用扇區,當一個扇區不能訪問的時候,磁碟控制器啟用備用扇區,這樣使得磁碟更健壯,不會因為一點點損壞就不能使用了。備用扇區可能相當的大。

連線I/O裝置

匯流排結構圖:CPU、主存、I/O裝置

I/O匯流排也是通過橋和CPU相連,這樣的設計有更大的相容性,比如USB(通行序列匯流排)可以連線多個不同裝置(印表機、滑鼠、鍵盤),傳送600M/s的資料(usb3.0)。圖形卡(GPU)代替CPU在顯示器上畫素顯示。特別的來講,磁碟是通過主機匯流排介面卡同io匯流排相連的。

訪問磁碟:(磁碟-主存-CPU)

對磁碟的資料訪問,並不是直接從磁碟到CPU,而是通過主存作為橋樑,達到快速訪問。我們現實生活中的橋,貌似也是這個作用。

當我們要讀取磁碟0xa0的內容,cup發出三道指令:1] 傳送一個命令,要求讀磁碟內容,要求讀完以後報告給CPU(中斷);2] 指明要讀取的具體邏輯塊號碼;3] 指明拷貝到主存的地址。

為什麼要使用中斷:一個1GHz的CPU時鐘週期是1ns,讀磁碟的16ms的時間內,可以執行1600萬條指令,這個時間如果只是等待的話就太浪費了。CPU發起讀指令以後,就不用管了,等到磁碟控制器將內容全部COPY到主存中,磁碟控制器發起一箇中斷,告訴CPU,不要做自己的其他事情了,你之前讓我讀磁碟的內容已經全部讀到主存中去了。

總結:現代計算機頻繁的使用SRAM的快取記憶體,試圖彌補越拉越大的儲存器與CPU之間的差距,我們接下來就來看看區域性性這一屬性是如何能彌補速度差的。

1.2 區域性性:以前用過的我還接著用


    我們講儲存器體系結構就會很好的理解區域性性,簡單的來說,我們的主存就是我們為了提高我們磁碟檔案的一個快取記憶體,因為我們知道這一時刻訪問到磁碟的資料可以下一時刻也會被訪問,這一位置被訪問的資料,鄰居位置也可能會被訪問。這也就是我們通常說的:時間區域性性和空間區域性性。

對程式資料訪問的區域性性

假設我們有這樣的一個二維陣列:

我們遍歷每個陣列求和,這樣的sum變數有很好是時間區域性性,因為我們訪問過一次,又接下來繼續在訪問。由於二維陣列是按照行的順序儲存的,按照步長為1的求和,也使得程式有很好的空間區域性性。

如果我們作一些改變,使得程式按照列的順序訪問:

�交換j和i的迴圈位置,使得程式按照列的方式求和,那麼程式的區域性性就相當的差了。

總結:

1.重複引用同一個變數的程式有良好的時間區域性性;

2.具有步調長度為k的引用模式程式,步調越小,空間區域性性越好。

 

我們一直在說具有良好的區域性性的程式將獲得更快的執行速度,究竟是什麼原因導致了這種執行速度的提升,我們將學習快取記憶體中的命中率和不命中率來量化區域性性的概念,這就是我們接下來要講到的內容了。

1.3 儲存器層次結構:理解命中率和不命中率


儲存器層次結構

越往上,代表的是訪問速度越快,當然儲存容量小,價格也非常的高。越往下,意味著訪問速度越慢,儲存容量大,價格相對便宜。通常我們CPU的暫存器是L1的快取記憶體,L1是L2的快取記憶體,以此類推。

基本快取原理:我們來看一個片段,下圖為L3作為主存的快取記憶體:

基本快取原理

上圖我們把k+1理解為主存,被劃分為16個塊來儲存資料,塊的大小是固定的。我們把K層理解成L3快取記憶體,任何時刻L3就是主存的一個子集。上圖我們能看出,L3只能儲存4個塊的資料,塊的大小保持和主存的大小一樣的。上圖中我們看到,L3中儲存的是主存中的4,9,14,3的資料。那麼什麼又是命中率和不命中率呢?

快取命中:當程式需要第k+1層資料塊14的時候,程式會在當前儲存的k層,尋找塊14的資料,剛好14在k層的話,就是一個快取命中,這比從k+1層讀取的速度要快很多。

快取不命中:當程式需要訪問到塊12的時候,在k層沒有該資料塊,就是一個快取不命中,這時候就會從k+1層中讀取塊12將其替換到k層的一個數據塊(覆蓋或驅逐一個已有的資料塊)。程式還是從k層訪問塊12。

放置策略:如果我們從k+1層中獲得的資料隨機的放置在k層,這樣的隨機放置就會導致訪問的效率降低,我們的放置策略是塊i必須放置在(imod4)中,也就是0,4,8,12會對映到同一個k層的塊0中。這就會導致一個衝突不命中,也就是說如果程式交替請求k+1層的0,4塊,由於會一直對映到k層的0塊中,這時候雖然k層有空餘的快取,但還是每次不命中。

總結:利用時間的區域性性,同一資料物件可能會被多次使用,一旦一個數據物件在第一次不命中的時候被拷貝到快取中,我們就會期望在接下來的訪問中有一系列的命中率。利用空間的區域性性,由於一個數據塊並不僅僅只有一個數據,而是一系列資料塊的集合,我們訪問到塊子集a的時候,可能會繼續訪問塊的子集b。

1.4 快取記憶體儲存器(整合在CPU內部的一個部件L1、L2、L3三級快取)


 

快取記憶體結構

① 通用快取記憶體儲存器內部結構

快取記憶體通用組織

快取記憶體是一個數組,每個組包含一個或多個行,每個行有一個有效位、一個標記位,以及資料塊。我們進行訪問的地址結構就是:t的標記位+s個組索引+b個塊偏移;

基本引數術語一覽表

② 直接對映快取記憶體(每個組只有一行的簡單訪問模式)

直接對映快取記憶體(E=1)。每個組只有一行

快取記憶體確定一個請求是否命中,然後抽出請求字的過程分三步:

(舉例:直接對映快取記憶體的抽取請求字的過程就像我們投遞快件一樣,組索引其實就像我們的郵政編碼,比如我們這裡的510824,然後找到編碼的組,也就是我的大位置(xx縣),然後看標記上寫的具體xx小區x棟樓,並且核實該地址是否有效(有效位1),兩項都滿足條件以後將該快件給快遞員投遞,快遞員到達具體xx小區x樓的時候就根據門牌號(偏移位)敲開你家的門。binggo,快遞到達)

1> 組選擇:很好理解,就是地址位中的組索引匹配快取記憶體中的組

組選擇

2> 行匹配和字抽取

行匹配和字選擇

行匹配主要是對有效位進行匹配,和標記位與快取記憶體中的標記位一致,這就是一個命中。最後的字抽取就簡單了,只是看地址後面的偏移值。

② 組相連快取記憶體(每組多於1行的快取記憶體)大致方法仍然一樣

組相連中的行匹配和字選擇

③ 全相連快取記憶體(只有一個組):只適合小規模的快取記憶體(翻譯備用緩衝器)

全相連快取記憶體中的行匹配和字選擇

④ 結構剖析(真正意義上的快取記憶體)

Intel Core I7快取記憶體層次結構

在實際的商用CPU中,將快取記憶體分為d-cache資料快取記憶體,i-cache指令快取記憶體和同一的快取記憶體,i7的架構中我們可以看出,L1分為資料和指令快取記憶體,共享L2快取記憶體,同時每個核共享L3快取記憶體

1.5 編寫快取記憶體友好的程式碼指導意見


1.對區域性變數的反覆引用是好的,因為編譯器能將它們快取在暫存器檔案中;

2.步長為1的引用模式是最好的;

3.多維陣列的訪問,注意使用行優先模式。

1.6 快取記憶體對程式效能的影響


① 儲存器山

儲存器山

儲存器的效能不能簡單的用一個數字來描述,如果實在要形容的話,是一座時間區域性性和空間區域性性構成的山。山峰和低谷的差別不是一個數量級。明智的程式設計師會試圖構造執行在山峰的程式而不是低谷。我們來看看這座儲存器山是如何畫出來的?

測試核心程式碼:

讀取陣列的內容到暫存器中

這段程式碼所做的事情,就是將data陣列的內容依次讀取到CPU的暫存器中。其中elems代表的是data的工作集大小也就是size時間區域性性,代表Y軸;而stride(步長)代表的是橫軸X;Z軸表示吞吐量,Mb/s。越往上吞吐量越大(紅色部分)。我們反覆以不同的size和stride值呼叫我們的核心測試程式碼,就會得到如上圖的儲存器山。

最高處的紅色山峰為L1,由於工作集(size)很小,能全部儲存在L1快取記憶體中,所以這時候即使stride很長,對於效能也沒太大的影響。

L2和L3、主存隨著stride的增加有明顯的坡度,空間區域性性下降。特別明顯的是,主存的藍色山峰,即使工作集很大(時間區域性性極地)其stride(空間區域性性)的影響也相當的明顯,最高與最低處相差7倍。也就是告誡我們,即使時間區域性性無法改變了,空間區域性性也可以使得程式的效能極大的提高。

② 從新排列迴圈以提高空間區域性性

考慮一對2*2陣列的相乘的問題:

陣列A乘以陣列B等於陣列C

矩陣的乘法是由三層巢狀的迴圈構成的,我們假設i是陣列A的迴圈計數,j是陣列B的迴圈計數,k是陣列C的迴圈計數:

三個不同的迴圈版本

我們看一下不同版本程式碼分析:

上傳分析圖我們簡單的說一下,核心的思想就是最後一個版本:kij執行的效率高很多。最主要的一點就是,在最後一個版本中,每次迴圈是按照行優先的順序一步步最後求得陣列C的值。

我們認為下列要求是不言而喻的,來結束這一章節的講解:

1.將你的注意力集中在內迴圈上,大部分計算的儲存器訪問都集中在這裡;

2.通過�按照資料實際在儲存器中存放的順序,以步長為1來訪問,空間區域性性最優;

3.一旦從一個儲存器中讀了一個數據出來,就儘可能多的利用他(kij版本)。