記住一個位元的兩種辦法
整個快取那一級,我們都靠著一個方便的速記:SRAM是快取用來蓋成的又快又小之物,DRAM則是底下又大又慢的主記憶體。這是真的,但它是個標籤,不是個解釋。這一級要掀開晶片本身。它們之間整個分裂,歸結到向晶片上每一顆電晶體問的一個問題:我們願意花多少顆電晶體去記住一個位元? 答「六顆」,你得到速度。答「一顆」,你得到便宜。其餘的一切——價格、密度、刷新、記憶體之牆——全都從這一個選擇流淌出來。
SRAM——靜態隨機存取記憶體——把每個位元存在一個由兩個反相器組成的小迴圈裡,它們互相握住對方的狀態,鎖死在兩個穩定位置之一。這跟暫存器檔用的是同一個雙穩態把戲,這也正是為什麼暫存器檔與 L1 快取都是用它做的。因為這個迴圈主動握住自己,只要通著電,位元就待著不動,毋須維護——這就是它名字裡那個「靜態」。代價是體積:一個經典 SRAM 單元要用六顆電晶體,所以每存一個位元,你都在替六個開關以及把它們連起來的導線付錢。
DRAM——動態隨機存取記憶體——不肯花六顆電晶體。它把每個位元存成一顆微小電容上的電荷,由單獨一顆存取電晶體看守:有電荷代表 1,空了代表 0。每個位元一顆電晶體加一顆電容。在單元層級上這大約比 SRAM 密六倍,這就是為什麼你機器裡有好幾 GB 的 DRAM,快取卻只有幾 MB 的全部原因。但電容是一只會漏的水桶——電荷會在幾毫秒內流光——所以 DRAM 是「動態」的:放著不管,它就遺忘。下一節我們就要替這個漏洞付帳。
那個漏洞,以及記得的代價
因為 DRAM 電容會漏,晶片必須週期性地讀出每一列再把電荷寫回去,趁它跌破那條把 1 和 0 分開的門檻之前,把它補滿。這個雜務叫刷新,它自動發生,典型上整個陣列每 64 毫秒一輪。刷新是 DRAM 為它的密度付的稅:它花掉一點能量,而且偶爾某一列正忙著被刷新時,恰好就是處理器想用它的時候,於是多出一個小停頓。SRAM 不欠這種稅——它的位元自己握住自己——這就是 SRAM 為什麼比較快(而不只是比較小)的一半原因。
SRAM 反應更快,還有一個比「不欠刷新」更深的原因。SRAM 單元主動驅動它的輸出——反相器迴圈用力推著位元線,於是一次讀取又快又有把握。DRAM 的讀取則是一場細膩的測量:打開存取電晶體,讓那一丁點儲存的電荷把一條大得多的位元線輕輕撥動一絲,再由一個感測放大器去偵測並重建它。那番感測要花時間,而且——因為讀取會把電容放掉——每一次 DRAM 讀取都必須立刻把值寫回去。SRAM 是把它的位元喊出來;DRAM 則得有人小心地去聽。
速度、密度、成本——三個互相打架的數字
現在把兩者並排,這個取捨就十分鮮明。SRAM 在延遲上以很大的差距勝出——在快取裡是幾百皮秒到一兩奈秒,碰到一個 DRAM 單元卻要幾十奈秒。DRAM 在密度、從而在每位元成本上勝出,同樣差距很大,因為一顆電晶體贏過六顆。你真的無法兩者兼得:讓 SRAM 快的那個東西(一個主動的六電晶體單元),正是讓它又大又貴的東西;而讓 DRAM 便宜的那個東西(一個單電晶體的電荷單元),正是讓它又慢又健忘的東西。
SRAM DRAM ------------ ----------------------- ----------------------- cell ~6 transistors 1 transistor + 1 capacitor density low (big cell) high (~6x denser) cost / bit high low latency very fast (<1-few ns) slow (tens of ns to a cell) refresh? no (static, self-held) yes (dynamic, leaks) volatile? yes yes used for register file, L1/L2/L3 main memory (the DIMMs) Same job (hold a bit) -- opposite engineering bargain. Six switches buy speed; one transistor buys cheapness.
這正是快取階層之所以存在的全部原因,而你剛學到的這些元件事實,就是它的地基。我們絕不可能用 SRAM 蓋出主記憶體——好幾 GB 的六電晶體單元會大得不可能、貴得要命。我們也絕不可能用 DRAM 蓋出快取又保持它快。所以架構師兩者都用:緊貼管線的一層薄薄 SRAM 換速度,它後面一片深深的 DRAM 海洋換容量,再用上一級那個區域性把戲,讓那片慢海洋多數時候感覺起來很快。SRAM 對 DRAM 不是一場有贏家的競賽;它是階層之所以分層的原因。
一步步讀出一個 DRAM 單元
值得把一次 DRAM 讀取慢慢走一遍,因為這一級其餘的部分都建立在這些動作上。單元坐在一格列與行的網格裡;要讀一個位元,你不是直接把它摘下來,而是先把它整列拉上一條感測放大器帶,再從那條帶上挑出你要的行。這個「先網格、再帶條」的舞步,正是為什麼 DRAM 有著下一篇導覽要細看的列與行時序。
- 開列(活化)。控制器選定一列;該列每顆電容的存取電晶體都把自己接上位元線,輕輕地把它們往上或往下推。
- 感測並鎖存。感測放大器偵測那些微小的推動,把每一個放大成完整的 0 或 1,並把整列握在一個晶片內的快速緩衝區裡(也就是列緩衝)。
- 讀出行。從那已鎖存的列裡,控制器選出它真正想要的行,把那些位元送出去。從同一個已開的列再讀更多行,現在很便宜——這一列已經升起來了。
- 寫回並關閉(預充電)。因為感測把電容放掉了,放大器把電荷復原,接著位元線被重置,好讓下一次能開另一列。
注意第 3 步裡藏著的那個不對稱。開一列是昂貴的部分;一旦它坐進了列緩衝,同一列的更多位元就幾乎免費而來。這一個事實——列一開,相鄰位元就便宜——正是空間區域性之所以划算的元件層級根源,也是為什麼當你照順序串流穿過記憶體、而不是到處亂跳時,記憶體跑得最快。快取那一級告訴你區域性很重要;這就是獎賞它的那部實體機器。
為什麼記憶體元件設下了上限
這裡是這整級誠實、略帶洩氣的標題:很多時候,設下效能上限的是記憶體元件,不是處理器。DRAM 晶片裡的一顆電容,受制於一套在邏輯不斷變快的同時幾乎沒怎麼進步的物理——這正是快取那一級的記憶體之牆,現在從元件那一側看過去。你可以買一顆核心數加倍、或時脈率更高的 CPU,但若你的程式在等 DRAM,那些多出來的核心就一輩子卡著空轉。這就是為什麼這麼多真實世界的效能工作,講的是尊重記憶體,而不是把它的時脈比過去。
在我們繼續往上爬之前,給兩個誠實的但書。第一,密度買不到速度:一個更密、更便宜的 DRAM 世代給你更多容量、往往也更多頻寬,但碰到一個新列的延遲,數十年來幾乎沒挪動過——容量與延遲是不同的軸,把它們混為一談是個經典錯誤。第二,這兩種記憶體都不完美。一道宇宙射線或一顆迷途粒子能翻轉一個儲存的位元——一個軟錯誤——在一台有著海量 DRAM 的機器裡,這很罕見但確實存在。伺服器靠 ECC 記憶體來防它,它多存一些檢查位元,好讓單位元翻轉被抓到並改正,用一點容量換來可信賴的資料。