在本章中,我們會先了解存儲技術(shù)(SRAM\DRAM\ROM\旋轉(zhuǎn)固態(tài)硬盤),描述這些存儲器是如何被組織成層次結(jié)構(gòu)的。接下來會談到什么是擁有良好局部性的程序以及編寫這樣的程序需要注意的問題。然后我們開始探究本質(zhì),為什么說擁有良好局部性的程序會執(zhí)行的更快。就要求我們要學(xué)習(xí)高速緩存,并教會大家理解程序的局部性的真正意義,使得你自己不僅僅遵守規(guī)則,而是了解其內(nèi)部原理獲取更大的自由。
1.1 存儲技術(shù)
① 隨機(jī)訪問存儲器
靜態(tài)RAM:(SRAM)用作高速緩存,通常只有幾兆,在CPU芯片上、下;硬件設(shè)計(jì)中,將每個位存在一個雙穩(wěn)定的存儲單元中,如下圖所示,只有在兩邊的時候保持穩(wěn)定性:
動態(tài)RAM:(DRAM)用作主存(我們通常說的機(jī)器的內(nèi)存),通常幾百、幾千兆。每個單位使用一個電容和一個訪問晶體管構(gòu)成,容易被干擾,有的加入有糾錯碼。系統(tǒng)需要周期性讀出,然后刷新重寫存儲器的每一位。
DRAM詳細(xì)構(gòu)造圖:
DRAM芯片被分割成16個超單元,每個超單元又由w個DRAM單元組成,一個16*w的DRAM共存儲16w位信息。超單元被組織成一個4*4的陣列,圖中的超單元地址用(2,1)表示。信息通過引腳流入和流出芯片,每個引腳攜帶1位信號,圖中有兩組引腳,其中data引腳為8個,能傳出或接受來自芯片的一個字節(jié)數(shù)據(jù),還有兩個addr引腳,攜帶2位的行列超單元地址。
訪問示例(我們來看看是如何訪問超單元(2,1)處的內(nèi)容)
分析:我們首先來思考一個問題,為什么要組成一個二維的數(shù)組,而不是一位數(shù)組,這樣訪問的速度就快很多啊。以上圖為例,我們?nèi)绻ㄔ煲粋€128位的DRAM,我們用一維數(shù)組實(shí)現(xiàn)的話,我們需要提供大量的地址引腳來提供訪問(硬件設(shè)計(jì)上太費(fèi)了)。
為了加快二維數(shù)組的訪問,存儲控制器在讀取(2,1)處的內(nèi)容的時候,使用addr先發(fā)送行地址2到DRAM芯片中,拷貝整個第二行的內(nèi)容到內(nèi)部緩沖區(qū)中,然后發(fā)送列地址1,從內(nèi)部行緩沖區(qū)中讀取1的地址內(nèi)容通過data發(fā)送到存儲控制器中去。
② 存儲器模塊
如圖所示是一個64M的主存,芯片編號0-7,每個芯片存儲8M的數(shù)據(jù),存儲器模塊將其組合起來,聚合內(nèi)存。將每單個芯片的超單元映射成主存地址A的各個字段。這樣控制器收到一個主存地址A的時候,存儲控制器將其選擇包含的具體芯片,將A轉(zhuǎn)換成(i,j)的形式,然后將(i,j)發(fā)送到芯片模塊中開始取數(shù)據(jù)。
備注:存儲在ROM設(shè)備中的程序通常稱為固件,當(dāng)一個計(jì)算機(jī)系統(tǒng)通電以后,它會運(yùn)行存儲在ROM中的固件。
③ 訪問主存(讀事務(wù)、寫事務(wù))
數(shù)據(jù)流通過總線,在CPU和DRAM中傳遞數(shù)據(jù),總線能攜帶:地址、數(shù)據(jù)和控制信號。
讀事務(wù):考慮當(dāng)我們執(zhí)行,movl A,%eax的情況,地址A的內(nèi)容會被加載到eax中去,總線發(fā)起讀事務(wù)(分三步):①CPU將A的地址總線放到系統(tǒng)總線上,橋作為中轉(zhuǎn)點(diǎn),將地址信號傳送到存儲器總線上去;②主存感覺到了存儲器總線上的地址信號,從存儲器總線上讀地址,并從主存中取出相應(yīng)的數(shù)據(jù),寫入到存儲器總線上去,橋?qū)?shù)據(jù)專遞到系統(tǒng)中線中去;③CPU感覺到了系統(tǒng)總線上的數(shù)據(jù),將數(shù)據(jù)拷貝到eax中。
I/O橋作為中轉(zhuǎn),將地址信號從系統(tǒng)總線轉(zhuǎn)到存儲器總線,然后又將數(shù)據(jù)從存儲器總線轉(zhuǎn)到系統(tǒng)總線。在這個過程中,CPU始終是從系統(tǒng)總線上發(fā)送地址,讀取數(shù)據(jù),主存始終是從存儲器總線上接受地址并發(fā)送數(shù)據(jù)。(寫事務(wù)是一個逆向過程不做講解)
④ 磁盤存儲 (硬盤)
構(gòu)造:
磁盤由盤片構(gòu)成,表面覆蓋的有磁性材料,中間是一個主軸,通過旋轉(zhuǎn)讀取和記錄數(shù)據(jù)。每組同心圓磁道分割的區(qū)域就是一個扇區(qū)。扇區(qū)之間是有間隙的,如圖:
磁盤讀寫操作:
磁盤以扇區(qū)為單位來讀寫數(shù)據(jù),對扇區(qū)的訪問時間由三個部分組成:尋道之間、旋轉(zhuǎn)時間、傳送時間。以圖a為例,當(dāng)我們要訪問同心圓磁道5的內(nèi)容時,尋道時間是指傳動手臂將讀寫頭移動到同心圓第五磁道的時間,旋轉(zhuǎn)時間指的是同心圓5開始讀取內(nèi)容的位置,如果手臂移動到第五磁道的時候讀寫位置剛過,就要等磁盤旋轉(zhuǎn)一圈之后再讀取;傳送時間,扇區(qū)第一個位處于讀寫頭的時候,讀寫該扇區(qū)的時間。(尋道時間和旋轉(zhuǎn)延遲大致相當(dāng))
磁盤為什么都是密封的?在傳動臂末端的讀/寫頭在磁盤表面高度大約0.!微米處的一層薄薄的氣墊上飛翔(就是字畫上這個意思),速度大約為80km/h。這可以比喻成將Sears?Tower(譯者注,一座位于芝加哥的108層和442米高的摩天大樓)放倒,然后讓它在距離地面2.5?cm(1英寸)的高度上飛行環(huán)繞地球,境地球一天只需要8秒鐘!在這樣小的間隙里,盤面上--粒微小的灰塵都像一塊巨石。如果讀/寫頭碰到了這樣的一塊巨石,讀/零頭會停下來,撞到盤面——所謂的讀/寫頭沖撞(head?crash)。
邏輯磁盤塊:
以我們正在使用的計(jì)算機(jī)為例,當(dāng)我們安裝的有g(shù)host軟件開始備份的時候,就會要求選擇備份文件的位置,我們看到的是1.1-1.6左右的可以選擇的硬盤,實(shí)際上磁盤雖然進(jìn)行了分區(qū),但本質(zhì)上仍然只有一塊磁盤。在磁盤中有一個小固件,磁盤控制器,維護(hù)著磁盤扇區(qū)之間的映射關(guān)系。假設(shè)我們要打開E:上的一個文件,控制器就會執(zhí)行一個快速表查找,將該處的內(nèi)容翻譯成(盤面、磁道、扇區(qū)),等到傳動臂移動到正確的位置時,將內(nèi)容讀到一個緩沖區(qū),然后拷貝的主存中去。
邏輯塊的作用:當(dāng)我們對磁盤進(jìn)行分區(qū)以前都要求我們進(jìn)行格式化,這樣做是讓磁盤控制器,讀取磁盤的基礎(chǔ)內(nèi)容,同時建立備用扇區(qū),當(dāng)一個扇區(qū)不能訪問的時候,磁盤控制器啟用備用扇區(qū),這樣使得磁盤更健壯,不會因?yàn)橐稽c(diǎn)點(diǎn)損壞就不能使用了。備用扇區(qū)可能相當(dāng)?shù)拇蟆?/p>
連接I/O設(shè)備
I/O總線也是通過橋和CPU相連,這樣的設(shè)計(jì)有更大的兼容性,比如USB(通行串行總線)可以連接多個不同設(shè)備(打印機(jī)、鼠標(biāo)、鍵盤),傳送600M/s的數(shù)據(jù)(usb3.0)。圖形卡(GPU)代替CPU在顯示器上像素顯示。特別的來講,磁盤是通過主機(jī)總線適配器同io總線相連的。
訪問磁盤:(磁盤-主存-CPU)
對磁盤的數(shù)據(jù)訪問,并不是直接從磁盤到CPU,而是通過主存作為橋梁,達(dá)到快速訪問。我們現(xiàn)實(shí)生活中的橋,貌似也是這個作用。
當(dāng)我們要讀取磁盤0xa0的內(nèi)容,cup發(fā)出三道指令:1] 發(fā)送一個命令,要求讀磁盤內(nèi)容,要求讀完以后報(bào)告給CPU(中斷);2] 指明要讀取的具體邏輯塊號碼;3] 指明拷貝到主存的地址。
為什么要使用中斷:一個1GHz的CPU時鐘周期是1ns,讀磁盤的16ms的時間內(nèi),可以執(zhí)行1600萬條指令,這個時間如果只是等待的話就太浪費(fèi)了。CPU發(fā)起讀指令以后,就不用管了,等到磁盤控制器將內(nèi)容全部COPY到主存中,磁盤控制器發(fā)起一個中斷,告訴CPU,不要做自己的其他事情了,你之前讓我讀磁盤的內(nèi)容已經(jīng)全部讀到主存中去了。
總結(jié):現(xiàn)代計(jì)算機(jī)頻繁的使用SRAM的高速緩存,試圖彌補(bǔ)越拉越大的存儲器與CPU之間的差距,我們接下來就來看看局部性這一屬性是如何能彌補(bǔ)速度差的。
1.2 局部性:以前用過的我還接著用
? ? 我們講存儲器體系結(jié)構(gòu)就會很好的理解局部性,簡單的來說,我們的主存就是我們?yōu)榱颂岣呶覀兇疟P文件的一個高速緩存,因?yàn)槲覀冎肋@一時刻訪問到磁盤的數(shù)據(jù)可以下一時刻也會被訪問,這一位置被訪問的數(shù)據(jù),鄰居位置也可能會被訪問。這也就是我們通常說的:時間局部性和空間局部性。
對程序數(shù)據(jù)訪問的局部性
假設(shè)我們有這樣的一個二維數(shù)組:
我們遍歷每個數(shù)組求和,這樣的sum變量有很好是時間局部性,因?yàn)槲覀冊L問過一次,又接下來繼續(xù)在訪問。由于二維數(shù)組是按照行的順序存儲的,按照步長為1的求和,也使得程序有很好的空間局部性。
如果我們作一些改變,使得程序按照列的順序訪問:
?交換j和i的循環(huán)位置,使得程序按照列的方式求和,那么程序的局部性就相當(dāng)?shù)牟盍恕?/p>
總結(jié):
1.重復(fù)引用同一個變量的程序有良好的時間局部性;
2.具有步調(diào)長度為k的引用模式程序,步調(diào)越小,空間局部性越好。
我們一直在說具有良好的局部性的程序?qū)@得更快的運(yùn)行速度,究竟是什么原因?qū)е铝诉@種運(yùn)行速度的提升,我們將學(xué)習(xí)高速緩存中的命中率和不命中率來量化局部性的概念,這就是我們接下來要講到的內(nèi)容了。
1.3 存儲器層次結(jié)構(gòu):理解命中率和不命中率
越往上,代表的是訪問速度越快,當(dāng)然存儲容量小,價(jià)格也非常的高。越往下,意味著訪問速度越慢,存儲容量大,價(jià)格相對便宜。通常我們CPU的寄存器是L1的高速緩存,L1是L2的高速緩存,以此類推。
基本緩存原理:我們來看一個片段,下圖為L3作為主存的高速緩存:
上圖我們把k+1理解為主存,被劃分為16個塊來存儲數(shù)據(jù),塊的大小是固定的。我們把K層理解成L3高速緩存,任何時刻L3就是主存的一個子集。上圖我們能看出,L3只能保存4個塊的數(shù)據(jù),塊的大小保持和主存的大小一樣的。上圖中我們看到,L3中保存的是主存中的4,9,14,3的數(shù)據(jù)。那么什么又是命中率和不命中率呢?
緩存命中:當(dāng)程序需要第k+1層數(shù)據(jù)塊14的時候,程序會在當(dāng)前存儲的k層,尋找塊14的數(shù)據(jù),剛好14在k層的話,就是一個緩存命中,這比從k+1層讀取的速度要快很多。
緩存不命中:當(dāng)程序需要訪問到塊12的時候,在k層沒有該數(shù)據(jù)塊,就是一個緩存不命中,這時候就會從k+1層中讀取塊12將其替換到k層的一個數(shù)據(jù)塊(覆蓋或驅(qū)逐一個已有的數(shù)據(jù)塊)。程序還是從k層訪問塊12。
放置策略:如果我們從k+1層中獲得的數(shù)據(jù)隨機(jī)的放置在k層,這樣的隨機(jī)放置就會導(dǎo)致訪問的效率降低,我們的放置策略是塊i必須放置在(imod4)中,也就是0,4,8,12會映射到同一個k層的塊0中。這就會導(dǎo)致一個沖突不命中,也就是說如果程序交替請求k+1層的0,4塊,由于會一直映射到k層的0塊中,這時候雖然k層有空余的緩存,但還是每次不命中。
總結(jié):利用時間的局部性,同一數(shù)據(jù)對象可能會被多次使用,一旦一個數(shù)據(jù)對象在第一次不命中的時候被拷貝到緩存中,我們就會期望在接下來的訪問中有一系列的命中率。利用空間的局部性,由于一個數(shù)據(jù)塊并不僅僅只有一個數(shù)據(jù),而是一系列數(shù)據(jù)塊的集合,我們訪問到塊子集a的時候,可能會繼續(xù)訪問塊的子集b。
1.4 高速緩存存儲器(集成在CPU內(nèi)部的一個部件L1、L2、L3三級緩存)
① 通用高速緩存存儲器內(nèi)部結(jié)構(gòu)
高速緩存是一個數(shù)組,每個組包含一個或多個行,每個行有一個有效位、一個標(biāo)記位,以及數(shù)據(jù)塊。我們進(jìn)行訪問的地址結(jié)構(gòu)就是:t的標(biāo)記位+s個組索引+b個塊偏移;
② 直接映射高速緩存(每個組只有一行的簡單訪問模式)
高速緩存確定一個請求是否命中,然后抽出請求字的過程分三步:
(舉例:直接映射高速緩存的抽取請求字的過程就像我們投遞快件一樣,組索引其實(shí)就像我們的郵政編碼,比如我們這里的510824,然后找到編碼的組,也就是我的大位置(xx縣),然后看標(biāo)記上寫的具體xx小區(qū)x棟樓,并且核實(shí)該地址是否有效(有效位1),兩項(xiàng)都滿足條件以后將該快件給快遞員投遞,快遞員到達(dá)具體xx小區(qū)x樓的時候就根據(jù)門牌號(偏移位)敲開你家的門。binggo,快遞到達(dá))
1> 組選擇:很好理解,就是地址位中的組索引匹配高速緩存中的組
2> 行匹配和字抽取
行匹配主要是對有效位進(jìn)行匹配,和標(biāo)記位與高速緩存中的標(biāo)記位一致,這就是一個命中。最后的字抽取就簡單了,只是看地址后面的偏移值。
② 組相連高速緩存(每組多于1行的高速緩存)大致方法仍然一樣
③ 全相連高速緩存(只有一個組):只適合小規(guī)模的高速緩存(翻譯備用緩沖器)
④ 結(jié)構(gòu)剖析(真正意義上的高速緩存)
在實(shí)際的商用CPU中,將高速緩存分為d-cache數(shù)據(jù)高速緩存,i-cache指令高速緩存和同一的高速緩存,i7的架構(gòu)中我們可以看出,L1分為數(shù)據(jù)和指令高速緩存,共享L2高速緩存,同時每個核共享L3高速緩存
1.5 編寫高速緩存友好的代碼指導(dǎo)意見
1.對局部變量的反復(fù)引用是好的,因?yàn)榫幾g器能將它們緩存在寄存器文件中;
2.步長為1的引用模式是最好的;
3.多維數(shù)組的訪問,注意使用行優(yōu)先模式。
1.6 高速緩存對程序性能的影響
① 存儲器山
存儲器的性能不能簡單的用一個數(shù)字來描述,如果實(shí)在要形容的話,是一座時間局部性和空間局部性構(gòu)成的山。山峰和低谷的差別不是一個數(shù)量級。明智的程序員會試圖構(gòu)造運(yùn)行在山峰的程序而不是低谷。我們來看看這座存儲器山是如何畫出來的?
測試核心代碼:
這段代碼所做的事情,就是將data數(shù)組的內(nèi)容依次讀取到CPU的寄存器中。其中elems代表的是data的工作集大小也就是size時間局部性,代表Y軸;而stride(步長)代表的是橫軸X;Z軸表示吞吐量,Mb/s。越往上吞吐量越大(紅色部分)。我們反復(fù)以不同的size和stride值調(diào)用我們的核心測試代碼,就會得到如上圖的存儲器山。
最高處的紅色山峰為L1,由于工作集(size)很小,能全部保存在L1高速緩存中,所以這時候即使stride很長,對于性能也沒太大的影響。
L2和L3、主存隨著stride的增加有明顯的坡度,空間局部性下降。特別明顯的是,主存的藍(lán)色山峰,即使工作集很大(時間局部性極地)其stride(空間局部性)的影響也相當(dāng)?shù)拿黠@,最高與最低處相差7倍。也就是告誡我們,即使時間局部性無法改變了,空間局部性也可以使得程序的性能極大的提高。
② 從新排列循環(huán)以提高空間局部性
考慮一對2*2數(shù)組的相乘的問題:
矩陣的乘法是由三層嵌套的循環(huán)構(gòu)成的,我們假設(shè)i是數(shù)組A的循環(huán)計(jì)數(shù),j是數(shù)組B的循環(huán)計(jì)數(shù),k是數(shù)組C的循環(huán)計(jì)數(shù):
我們看一下不同版本代碼分析:
上傳分析圖我們簡單的說一下,核心的思想就是最后一個版本:kij執(zhí)行的效率高很多。最主要的一點(diǎn)就是,在最后一個版本中,每次循環(huán)是按照行優(yōu)先的順序一步步最后求得數(shù)組C的值。
我們認(rèn)為下列要求是不言而喻的,來結(jié)束這一章節(jié)的講解:
1.將你的注意力集中在內(nèi)循環(huán)上,大部分計(jì)算的存儲器訪問都集中在這里;
2.通過?按照數(shù)據(jù)實(shí)際在存儲器中存放的順序,以步長為1來訪問,空間局部性最優(yōu);
3.一旦從一個存儲器中讀了一個數(shù)據(jù)出來,就盡可能多的利用他(kij版本)。