JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

記憶體之牆與記憶體階層

我們的管線可以每個週期完成一條指令——但前提是它需要的資料就在手邊。可惜的是,記憶體住得很遠,總是姍姍來遲。這篇導覽要認識記憶體之牆,以及面對它的巧妙解答:一座記憶體的階層,又小又快的疊在又大又慢的上面,給出一種「同一塊記憶體既快又大」的錯覺。

一顆無米下鍋的處理器

上一級結束時,我們已經有了一台快機器。管線讓好幾條指令同時在途,再加上前饋與一個還不錯的分支預測器,它可以接近每個時脈週期退役一條指令。但管線是一張飢餓的嘴:一條載入指令現在就要它的資料,就在它開口的那個週期裡要到,否則整條生產線就會停下來枯等。所以效能真正的問題,不再是「CPU 有多快?」,而變成「我們能餵它多快?」

這裡有個讓人不舒服的事實。世上存在兩種記憶體,它們買到的是相反的東西。SRAM——暫存器檔就是用它做的——美妙地快,卻昂貴又笨重,所以我們只負擔得起一丁點。DRAM,也就是你機器裡的主記憶體,便宜又密集,所以我們能擁有好幾 GB——但它,要碰到它輕易就比處理器自己的時脈慢上一百倍。你無法擁有一塊同時又大、又便宜、又快的記憶體。物理與經濟學都不允許。

記憶體之牆

從前並沒有這麼痛。早年裡,一個處理器週期和一次記憶體存取差不多一樣久,所以一個載入大約和一個加法一樣貴。但數十年來,處理器速度爬升得遠比記憶體速度快——晶片的吞吐量翻倍再翻倍,DRAM 的延遲卻只慢慢地往下挪。差距一年比一年大,直到如今碰一次主記憶體要花上數百個處理器週期。這道越來越大的鴻溝有個名字:記憶體之牆。它是每一顆快處理器最終都會撞上的那堵牆。

想想為什麼這是致命的。回想效能鐵律:執行時間 = 指令數 x CPI x 週期時間。如果每十個載入裡有一個要花 200 個週期才回得來,而管線只是乾等,那麼你的有效 CPI 主宰權,就不在那些可愛的一週期指令手上,而落到了那些罕見卻災難性的指令身上。一顆 4 GHz 的處理器,若大半輩子都卡在記憶體上空轉,實際上並不比一顆慢得多的處理器快。這也是為什麼光是更高的時脈率並不能讓機器更快——贏了時脈速度卻輸了記憶體競賽,什麼也買不到。

點子:一座階層,而非一塊記憶體

既然沒有任何單一記憶體能又快又大,我們就不再試著造出一塊,而是改去疊好幾塊。緊挨著處理器的,是一小塊飛快的 SRAM 快取。它後面是又大又慢的 DRAM 主記憶體。後面,又是一塊更大、慢得多的磁碟。每一層都比它上面那層更大、更慢,而記憶體階層正是這種一層層的安排。處理器永遠只跟最頂層說話;當它在那裡找不到,請求就悄悄地往下穿到下面那層去。

用日常的畫面來想。你的書桌上放著你此刻正在用的那幾本書——很小,但桌上的一切都伸手可及。你身後的書架放得更多;跨過校園的那座圖書館則放著一切。你把最常用的書留在桌上,於是幾乎不必走去圖書館;而當你真的走一趟時,你會抱回一小疊,而不是單獨一頁。快取就是那張書桌;主記憶體就是那座圖書館。整個把戲都押在一個賭注上:你會一再去拿你剛剛才拿過的東西——這個賭注我們稱為區域性原理,也正是下一篇導覽的主題。

  level        made of   typical size   typical access   role
  ---------    -------   ------------   --------------   -------------------------
  registers    SRAM      ~ 1 KB         < 1 cycle        held inside the CPU
  L1 cache     SRAM      ~ 32-64 KB     ~ 1-4 cycles     the desk: hottest data
  L2 cache     SRAM      ~ 256KB-1 MB   ~ 10-15 cycles   bigger desk drawer
  L3 cache     SRAM      ~ 8-32 MB      ~ 30-50 cycles   shared shelf for all cores
  main memory  DRAM      ~ 8-64 GB      ~ 100-300 cyc    the campus library
  SSD / disk   flash     ~ 1 TB+        ~ 100k+ cycles   distant warehouse

  Each step down: roughly 10x bigger, and many times slower.
  The processor asks the top; a miss falls through to the next level.
一個典型的記憶體階層。尺寸與時序都是粗略的、數量級的數字,依機器而異,但那個形狀——又小又快在上、又大又慢在下——是普世不變的。

為什麼階層感覺像一塊又快又大的記憶體

魔法是一種階層製造出來的錯覺:對程式而言,它看起來就像一塊單一的記憶體,跟最底層一樣大,卻幾乎跟最頂層一樣快。這感覺像作弊,而它之所以行得通,只因為大多數存取都命中在那個又小又快的層。為了量度它做得多好,架構師用一條簡潔的公式,叫平均記憶體存取時間(AMAT)。對於記憶體前面只有一塊快取的情形:

整條公式不過就是:AMAT = 命中時間 + 未命中率 x 未命中代價。給它套上數字。假設一次命中花 1 個週期,你有 5% 的存取會未命中(也就是每 20 次有 19 次在桌上找到),而一次未命中要花 200 個週期去 DRAM 取回。那麼 AMAT = 1 + 0.05 x 200 = 1 + 10 = 平均 11 個週期。若完全沒有快取,每一次存取都得付那整整約 200 個週期——所以一張能接住 95% 請求的書桌,把 200 變成了 11,大約快了 18 倍,而這既沒有用上更快的記憶體,也沒有用上更快的時脈。

把那條公式慢慢讀,因為它是這整級的靈魂。命中時間是資料已經在桌上時的代價——便宜,而且幾乎每次存取都要付。未命中代價則是資料不在時,走那趟遠路到下層的代價。所以只要未命中很罕見,AMAT 就由命中時間主宰。這是架構師版本的「讓常見情況變快」:把力氣全倒在讓命中既便宜又頻繁上,那麼偶爾一次昂貴的未命中幾乎不會撼動平均值。注意這條公式甚至告訴了你能拉的三根桿子——壓低命中時間、壓低未命中率、或壓低未命中代價——而這一級後面每一篇導覽,其實都是在其中一根上施力的辦法。

快取疊著快取,以及該留心的事

一張書桌通常不夠。真正的機器用多層快取:一個極小、近乎即時的 L1 坐在離管線最近的地方,後面是一個寬敞些但稍慢的 L2,再來是一個由所有核心共用的大 L3。當 L1 未命中,它會先問 L2,才去打擾 DRAM;於是 AMAT 一層套一層,每一層的未命中代價就是下一層的整個存取時間。這道階梯,正是一顆晶片如何把常見情況維持在一兩個週期,同時又能撐住好幾 MB 的工作資料而完全不必碰主記憶體的辦法。

在我們繼續往上爬之前,先給兩個誠實的警告。第一,快取不是免費的速度——它的好處必須靠區域性掙來。一段到處亂碰記憶體、從不重用任何東西的程式,會不停未命中,跑起來比整潔的程式慢上好幾倍,儘管兩者算出的答案一模一樣;我們會在最後一篇導覽裡精準地看到這一點。第二,這座階層是貨真價實、坐在處理器與 DRAM 之間的硬體,它跟虛擬記憶體——也就是上一級那套位址轉換機制——是兩回事。它們會合作,但一個藏的是延遲,另一個提供的是私有的位址空間與保護——別把它們混為一談。