一顆無米下鍋的處理器
上一級結束時,我們已經有了一台快機器。管線讓好幾條指令同時在途,再加上前饋與一個還不錯的分支預測器,它可以接近每個時脈週期退役一條指令。但管線是一張飢餓的嘴:一條載入指令現在就要它的資料,就在它開口的那個週期裡要到,否則整條生產線就會停下來枯等。所以效能真正的問題,不再是「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 之間的硬體,它跟虛擬記憶體——也就是上一級那套位址轉換機制——是兩回事。它們會合作,但一個藏的是延遲,另一個提供的是私有的位址空間與保護——別把它們混為一談。