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

DDR、通道與頻寬

你已經認識一顆 DRAM 晶片的內臟——記憶庫、列、行、列緩衝區,以及 RAS/CAS 那支舞。這篇導覽把鏡頭拉遠,看整個記憶體系統:DDR 如何在時脈的兩個邊緣都搬資料、DIMM 與通道如何把資料路徑加倍,以及把這一切綁在一起的那道誠實公式——頻寬 = 寬度 x 速率 x 通道數。

從一顆晶片到同步匯流排:SDRAM 與 DDR

上一篇導覽裡,我們爬進了單一顆 DRAM 晶片:位元以電荷的形式停泊在微小的電容上,被組織成一個個記憶庫,每個記憶庫是一格格列與行的網格,還有一個快速的列緩衝區握著你最近開啟的那一列。我們看過 RAS/CAS 那支舞——閂進一個列位址以開啟一列,再閂進行位址,從列緩衝區裡把一塊塊資料讀出來。現在我們從晶片往後退一步,問一個系統層級的問題:處理器到底是怎麼跟一整面牆的這種晶片溝通、快到足以餵飽自己的?

第一個大想法是讓記憶體同步。早期的 DRAM 是非同步的——控制器戳一下位址與控制線,然後等一個固定、規格上標明的奈秒數,資料才會出現。同步式 DRAM(SDRAM)則改成與記憶體控制器共用一個時脈,並隨著節拍應答。聽起來像是個小改動,但這跟我們在 CPU 內部看過的同步設計是同一招:一旦兩邊都跟著同一個時脈踏步,你就能把請求做成管線、跨記憶庫彼此重疊、並以整數個週期來推理時序,而不是用模糊的類比延遲。SDRAM 把記憶體匯流排變成了一條紀律嚴明、可預測的生產線。

DDR——雙倍資料速率——加上了下一個轉折。一個普通的時脈訊號每個週期升一次、再降一次;一條樸素的同步匯流排只在每個上升邊緣搬一塊資料。DDR SDRAM 在上升邊緣下降邊緣各搬一塊,於是它每個時脈週期搬兩次資料,卻完全沒把時脈調高。想像一個每秒滴答一千下的節拍器:單倍資料速率在每一下「滴」放上一個包裹(每秒 1000 個),而 DDR 在「滴」和「答」各放一個(每秒 2000 個),用的是同一台節拍器。這就是為什麼記憶體是以「傳輸速率」(每秒百萬次傳輸,MT/s)來標示,而它是時脈頻率的兩倍。

插條與車道:DIMM、rank 與通道

單一顆 DRAM 晶片很窄——它的資料腳一次也許只交出 4 或 8 個位元。一顆現代 CPU 想要的是一整條快取行(常是 64 位元組),而且想要它以一大口平行的方式吞下。解法是把許多晶片綁在一起。DIMM(雙列直插記憶體模組)就是你卡進插槽的那條插條:一塊小小的印刷電路板,載著好幾顆 DRAM 晶片,並把它們的資料腳接成一條寬匯流排——典型上每次傳輸一起送出 64 位元(8 位元組)的資料(若它帶有 ECC 錯誤檢查則是 72 位元,那是第 5 篇導覽的話題)。

在一條 DIMM 之內,那些一起組成完整 64 位元寬群組的晶片,被稱為一個 rank。一條 DIMM 可能在同一組資料線後面疊著一個、兩個、或更多 rank;控制器用一個晶片選擇(chip-select)訊號挑要跟哪個 rank 講話。rank 之所以重要,是因為當一個 rank 正忙著收尾一次傳輸、或正在恢復時,控制器可以對另一個 rank 發出命令——這是又一層的重疊,疊在每顆晶片內部那層記憶庫層級的重疊之上。rank 與記憶庫談的都是同一件事:總是讓記憶體的某一部分隨時能應答,好把資料線一直餵飽。

現在來看最大的乘數:記憶體通道。一個通道是記憶體控制器與一組 DIMM 之間一條完整、獨立的路徑——它有自己的 64 條資料線、自己的命令匯流排、自己的控制器邏輯。一套雙通道系統把兩條這樣的路徑並排跑,使每次傳輸送出的資料加倍;一台四通道或八通道的伺服器則再往上乘。通道才是那個搶眼的記憶體頻寬真正的來源。想像超市的結帳台:一個手腳快的收銀員(一個通道)掃描速度終究有限,但開出八條結帳道,整間店就清空得快得多。癥結在於——你必須把顧客分散到各條道上去,而這正好成了控制器的工作。

那一道公式:頻寬 = 寬度 x 速率 x 通道數

上面講的一切,全都收攏成一道誠實的方程式,用來算峰值記憶體頻寬——記憶體系統每秒能串流出的最大位元組數。把三樣東西相乘:每次傳輸有多(以位元組計的匯流排寬度)、傳輸發生得多(傳輸速率,已經含進了 DDR 的雙倍抽取)、以及有多少條獨立通道並行地跑。那就是頻寬 = 寬度 x 速率 x 通道數。它是記憶體那根管子的吞吐量,跟任何單次存取要花多久毫無關係——頻寬講的是流量,不是延遲。

Worked example: a dual-channel DDR4-3200 desktop

  bus width per channel = 64 bits = 8 bytes
  transfer rate         = 3200 MT/s   (1600 MHz clock, double-pumped)
  channels              = 2

  per-channel bandwidth = 8 bytes  x  3200 M/s  = 25.6 GB/s
  total bandwidth       = 25.6 GB/s x 2 channels = 51.2 GB/s

Double the channels (4) or move to DDR5-6400 and the number
climbs the same way -- it is just width x rate x channels.
Note: this is the PEAK ceiling, never the steady speed you see.
峰值頻寬就是三個因素相乘。真實的持續頻寬會更低——列緩衝區未命中、更新週期、記憶庫衝突都會偷走週期——所以把 51.2 GB/s 當成一個你逼近的上限,而不是一個你真能達到的速度。

對峰值與你真正拿到的之間那道落差要誠實。這道公式假設傳輸永不停歇——但它們會停。每當一次存取落在一個關著的列上,你就得先付完整的「RAS 再 CAS」開列,資料才開始流;晶片會週期性地為了更新而暫時失聰;而兩個指向同一個記憶庫的請求必須排隊等候。真實工作負載上的持續頻寬,往往只有印出來那個峰值的 60% 到 80%。峰值數字是個貨真價實的上限——拿來比較零件、以及之後的屋頂線模型都好用——但沒有任何程式真的住在那個天花板上。

控制器的手藝:為了平行而排程

那麼多通道、rank 與記憶庫,只有當有某個東西讓它們同時全都忙起來時才有用——而那個東西就是記憶體控制器,坐在最後一級快取與 DIMM 之間那個忙碌的交通警察。它收到一串讀寫請求(每一個都是逃出晶片內快取的快取未命中),而它並不是單純照抵達順序去服務。它反而把這些請求重新排序,去贏兩個獎賞:命中一個已經開著的列緩衝區,以及把活分散到彼此獨立的記憶庫與通道上、好讓好幾次存取同時在途。這種記憶庫層級的平行,正是把原始的寬度與速率變成真正送出的頻寬的關鍵。

  1. 一次載入錯過了每一級快取,於是一個 64 位元組的快取行請求帶著一個實體位址抵達記憶體控制器。
  2. 控制器把那個位址解碼成通道、rank、記憶庫、列與行——並選擇一種映射方式,讓連續的快取行噴灑到不同的通道與記憶庫上,而不是全堆在同一個上面。
  3. 若那一列已經坐在該記憶庫的列緩衝區裡,這就是一次列緩衝區命中:只發 CAS,資料幾乎立刻就流出來。
  4. 若開著的是另一列,那就是一次衝突:關掉舊的列、對新的列發 RAS、再 CAS——慢路徑,付完整的 RAS/CAS 延遲。
  5. 與此同時,控制器對其他記憶庫與通道發出命令,好讓它們的存取與這一次重疊,並把讀與寫成批處理,以避開共享資料匯流排上代價高昂的方向切換。

這就是為什麼兩支指令數一模一樣的程式,記憶體速度卻可能天差地別——跟對快取友善的程式碼是同一課,只是又低了一層。以良好空間區域性走訪記憶體的程式碼,會不斷命中開著的列緩衝區,並讓控制器乾淨俐落地跨記憶庫條帶化;而東跳西跳、隨機亂走的程式碼,幾乎每次都逼出一次列衝突,並把存取串列化到同一個記憶庫上。硬體頻寬是固定的,但你賺到多少,取決於你的存取型態。控制器很聰明,但它救不了一個跟記憶體結構作對的存取型態。

這把我們帶到哪:頻寬長大了,延遲沒有

把整個故事疊起來,一幅清醒的圖像就浮現了。橫跨歷代 DDR,頻寬已翻了許多倍——雙倍抽取、越來越高的傳輸速率、更多通道、更寬的匯流排。但抵達一次全新、關著列的存取的第一個位元組的延遲,卻改善得令人心痛地少:一個 DRAM 陣列的 RAS/CAS 開列至今仍是幾十奈秒,跟好幾世代前差不多,因為它是由類比物理決定的——替位元線充電、喚醒感測放大器——這種事根本不會像數位邏輯那樣縮小。我們買到了一條消防水管,但第一滴水抵達的時間幾乎沒變。

那道分裂正是替這一級接下來鋪路的關鍵句。頻寬可以用平行買來——更多通道、更多記憶庫,整篇導覽談的那套結構。延遲買不來:它是一道物理的地板。而與此同時,處理器的胃口只增不減——更多核心、更寬的管線,全都同時嗷嗷待哺。CPU 算得多快,與記憶體餵得多快,這兩者之間越裂越寬的鴻溝,就是記憶體牆,正是下一篇導覽的主題;在那裡我們會遇見高頻寬記憶體與 3D 堆疊記憶體——那些藉著正面攻擊這道方程式的頻寬那一側、所造出的激進解答。