
一、生活里的例子圖書館的密集書庫一座大型圖書館書不放在閱覽室而是放在后面的密集書庫你要一本書管理員先走到對應的書庫房間把一整排書架推出來密集書架平時是合攏的從這一排里取出你要的那本如果下一本書在同一排直接再拿就行很快如果在另一排要先把這排推回去再推出新的一排慢很多另外書會慢慢受潮管理員要定期翻曬圖書館DRAM一間書庫Bank推出一整排書架激活ACT把一整行讀到行緩沖區(qū)推出來的那一排行緩沖區(qū)Row Buffer從這排里拿某本書**讀READ**某一列推回去預充電PRE關閉這一行定期翻曬刷新REF好幾間書庫可以同時工作多個 Bank 并行記住兩點就理解了 DRAM 的大部分性能特征同一行里連續(xù)取很快換行很慢。二、層級結構從內(nèi)存條到電容CPU 的內(nèi)存控制器 └─ 通道Channel 雙通道 兩條獨立的數(shù)據(jù)通路 └─ 內(nèi)存條DIMM └─ Rank 一組芯片共同組成 64 位寬的數(shù)據(jù) └─ 芯片Chip 內(nèi)存條上那一顆顆黑色方塊 └─ Bank Group → Bank └─ 存儲陣列行Row× 列Column └─ 單元1 個晶體管 1 個電容層級典型規(guī)模DDR4 為例作用通道臺式機常見 2 個帶寬疊加Rank每條 12 個8 顆 ×8 芯片拼成 64 位Bank每顆芯片 16 個4 個 Bank Group可以并行工作行每個 Bank 幾萬行激活的基本單位行大小每顆芯片約 1KB整個 Rank 約 8KB一次激活打開這么多數(shù)據(jù)存儲陣列長什么樣列 0 列 1 列 2 ... 列 N 行 0 ───── ● ● ● ● ← 字線Word Line 行 1 ───── ● ● ● ● 行 2 ───── ● ● ● ● ... │ │ │ │ 位線 位線 位線 位線Bit Line │ │ │ │ ┌─────────────────────────┐ │ 靈敏放大器 行緩沖區(qū) │ ← 一次裝下整整一行 └─────────────────────────┘ │ 列選擇 ▼ 數(shù)據(jù) I/O字線橫著走選中某一行這一行所有晶體管一起打開位線豎著走把每個電容的電荷引到底部的靈敏放大器靈敏放大器就是行緩沖區(qū)放大信號同時暫存這一整行三、一次讀取的完整過程三個核心命令命令做什么ACT激活選中一行 → 電荷流到位線 → 靈敏放大器放大并鎖存整行READ / WRITE從行緩沖區(qū)里選出某幾列傳出或寫入PRE預充電把行數(shù)據(jù)寫回電容關閉這一行位線恢復到中間電壓準備下一次激活上一篇講過讀取是破壞性的激活時電容里的電荷已經(jīng)被分走正是靈敏放大器把放大后的結果寫回單元數(shù)據(jù)才得以保留。三種情況三種速度情況需要的步驟速度行命中要的行已經(jīng)在行緩沖區(qū)里READ最快行空閑Bank 里沒有打開的行ACT → READ中等行沖突Bank 里打開的是別的行PRE → ACT → READ最慢行命中 [READ]────→ 數(shù)據(jù) 行空閑 [ACT]──────[READ]────→ 數(shù)據(jù) 行沖突 [PRE]──────[ACT]──────[READ]────→ 數(shù)據(jù)四、時序參數(shù)內(nèi)存條上那串數(shù)字內(nèi)存條標簽上常見DDR4-3200 CL16-18-18-38這樣的字樣參數(shù)全稱含義CLCAS Latency發(fā)出 READ 后等多久數(shù)據(jù)出來tRCDRAS to CAS Delay發(fā)出 ACT 后等多久才能 READtRPRow Precharge發(fā)出 PRE 后等多久才能 ACT 新的一行tRASRow Active Time一行至少要打開多久才能關這些數(shù)字的單位是時鐘周期要換算成納秒才有意義DDR4-3200數(shù)據(jù)速率 3200 MT/s時鐘 1600 MHz 一個周期 1 / 1600MHz 0.625 ns CL16 → 16 × 0.625 10 ns tRCD18 → 11.25 ns tRP18 → 11.25 ns情況估算只算 DRAM 本身行命中CL ≈ 10 ns行空閑tRCD CL ≈ 21 ns行沖突tRP tRCD CL ≈ 32 ns再加上 CPU 查完各級緩存、經(jīng)過內(nèi)存控制器排隊的時間一次真正的內(nèi)存訪問通常在 60100 ns。一個常被誤解的點內(nèi)存CL換算成納秒DDR4-2400 CL151512.5 nsDDR4-3200 CL161610 nsDDR5-6000 CL303010 nsDDR5 的 CL 數(shù)字看起來是 DDR4 的兩倍但因為時鐘更快實際延遲差不多。幾十年來內(nèi)存的帶寬提升了幾十倍延遲卻幾乎一直停在十幾納秒的量級 —— 電容充放電、長導線傳信號這些物理過程很難再快多少。五、帶寬DDR、預取和突發(fā)DDR 是什么意思Double Data Rate雙倍數(shù)據(jù)速率時鐘的上升沿和下降沿各傳一次數(shù)據(jù)。所以 DDR4-3200 的時鐘是 1600MHz每秒傳輸 3200M 次。帶寬計算DDR4-3200單通道 64 位 3200M 次/秒 × 8 字節(jié) 25.6 GB/s 雙通道51.2 GB/s存儲陣列那么慢怎么喂飽這么快的接口靠預取Prefetch存儲陣列內(nèi)部一次并行讀出很多位再由接口快速地一個接一個送出去。代際預取一次突發(fā)BurstDDR48nBL8連續(xù) 8 次傳輸 × 64 位 64 字節(jié)DDR516n每條分成兩個 32 位子通道BL16 × 32 位 64 字節(jié)64 字節(jié)正好是 CPU 緩存行的大小。這不是巧合CPU 每次缺失緩存就向內(nèi)存要一整行 64 字節(jié)內(nèi)存一次突發(fā)剛好送完。所以哪怕你只讀一個int實際搬運的也是 64 字節(jié)。六、多 Bank 并行把慢操作藏起來一個 Bank 在做 PRE 和 ACT 時其他 Bank 可以同時工作。內(nèi)存控制器會交錯發(fā)命令時間 → Bank 0 [ACT]────[READ]────[PRE] Bank 1 [ACT]────[READ]────[PRE] Bank 2 [ACT]────[READ]────[PRE] 數(shù)據(jù)總線 ███ ███ ███ ← 幾乎一直在傳數(shù)據(jù)單個 Bank 很慢但多個 Bank 輪流上數(shù)據(jù)總線就能保持忙碌。這和網(wǎng)絡里一條連接跑不滿帶寬就開多條并發(fā)是同一個思路。地址映射物理地址怎么拆內(nèi)存控制器把物理地址的各個位拆給不同層級具體映射因平臺而異下面只是示意物理地址│ 行 │ Rank │ Bank │ 通道 │ 列 │ 緩存行內(nèi)偏移 │ 高位 低位設計思路是連續(xù)的地址先在同一行內(nèi)的列上推進再分散到不同的通道和 Bank。于是順序訪問一大塊內(nèi)存時既能享受行命中又能利用多通道、多 Bank 并行。七、刷新倉庫的日常維護上一篇講過電容會漏電。具體怎么刷新參數(shù)DDR4 典型值含義刷新周期64 ms高溫時 32 ms所有行都要在這段時間內(nèi)刷新一遍REF 命令數(shù)8192 次 / 64ms把工作分成 8192 小份tREFI約 7.8 μs平均每隔這么久發(fā)一次 REFtRFC幾百納秒容量越大越長每次刷新期間該 Rank 不能被訪問時間 → ──正常訪問──[REF]──正常訪問──[REF]──正常訪問── ↑ 幾百 ns 不可用 ↑ 每 7.8μs 來一次估算一下代價幾百納秒 ÷ 7.8 微秒 ≈百分之幾的時間花在刷新上。芯片容量越大要刷新的行越多這個比例越高所以新標準一直在改進刷新方式例如 DDR5 支持按 Bank 刷新刷新一個 Bank 時其他 Bank 照常工作。溫度越高漏電越快所以超過 85°C 時刷新頻率要加倍 —— 這也是內(nèi)存過熱會變慢甚至出錯的原因之一。八、內(nèi)存控制器倉庫的調(diào)度員現(xiàn)代 CPU 內(nèi)部集成了內(nèi)存控制器它負責工作說明翻譯地址物理地址 → 通道 / Rank / Bank / 行 / 列發(fā)命令ACT、READ、WRITE、PRE、REF并嚴格遵守所有時序參數(shù)請求重排優(yōu)先處理行命中的請求常見策略 FR-FCFS先服務行命中再按到達順序行策略開頁讀完先不關賭下一次還是這行閉頁讀完立刻關減少沖突代價安排刷新在合適的時機插入 REF糾錯支持 ECC 時負責檢查和糾正錯誤請求重排的思路和前面網(wǎng)絡負載篇的帶寬預算 優(yōu)先級很像資源有限先做代價最小、收益最大的事同時防止某些請求被餓死。九、各種 DRAM 家族原理都是1 個晶體管 1 個電容區(qū)別在于接口和封裝的取舍類型用在哪特點DDR4 / DDR5臺式機、服務器內(nèi)存條容量大、通用DDR5 把穩(wěn)壓芯片PMIC放到內(nèi)存條上芯片內(nèi)部帶 On-die ECCLPDDR4X / LPDDR5手機、輕薄本低電壓、低功耗通常焊在主板上或與芯片封裝在一起GDDR6 / GDDR6X顯卡顯存位寬大、頻率高追求帶寬功耗和成本較高HBM高端 GPU、AI 加速卡多層芯片垂直堆疊接口位寬上千位帶寬極高注意 DDR5 的 On-die ECC 只在芯片內(nèi)部糾錯用于彌補工藝縮小帶來的可靠性下降不等于服務器上那種完整的 ECC 內(nèi)存。十、和游戲性能的關系為什么內(nèi)存訪問模式這么重要一組數(shù)量級訪問位置大約耗時換算成 4GHz CPU 周期寄存器 1 ns1L1 緩存~1 ns~4L2 緩存~35 ns~15L3 緩存~1020 ns~50DRAM~60100 ns~300一次緩存缺失去訪問 DRAMCPU 可能要空等幾百個周期。這幾百個周期里本來能做上千次加法。好的訪問模式 vs 壞的訪問模式模式效果順序遍歷數(shù)組每 64 字節(jié)缺失一次后面全命中CPU 硬件預取器還能提前拉數(shù)據(jù)DRAM 端多是行命中隨機跳著訪問幾乎每次都缺失緩存DRAM 端頻繁行沖突鏈表、指針滿天飛的對象圖下一個地址要等上一個讀完才知道無法預取延遲一次次串行疊加動手驗證順序 vs 隨機usingSystem;usingSystem.Diagnostics;classMemoryAccessTest{staticvoidMain(){constintN125;// 3200 多萬個 int約 128MB遠大于 L3vardatanewint[N];varseqnewint[N];varrndnewint[N];for(inti0;iN;i){data[i]i;seq[i]i;rnd[i]i;}// 打亂索引Fisher-YatesvarrnewRandom(42);for(intiN-1;i0;i--){intjr.Next(i1);(rnd[i],rnd[j])(rnd[j],rnd[i]);}Console.WriteLine($順序訪問:{Measure(data,seq)}ms);Console.WriteLine($隨機訪問:{Measure(data,rnd)}ms);}staticlongMeasure(int[]data,int[]index){varswStopwatch.StartNew();longsum0;for(inti0;iindex.Length;i)sumdata[index[i]];// 訪問的元素數(shù)量完全相同sw.Stop();if(sum42)Console.WriteLine();// 防止被編譯器優(yōu)化掉returnsw.ElapsedMilliseconds;}}兩次循環(huán)做的加法一樣多讀的元素也一樣多但隨機訪問通常會慢數(shù)倍甚至十幾倍具體倍數(shù)取決于 CPU、內(nèi)存和緩存大小。差距幾乎全部來自緩存缺失和 DRAM 的行沖突。在 Unity 里的體現(xiàn)做法為什么快 / 慢?數(shù)組 /NativeArray連續(xù)存放數(shù)據(jù)順序遍歷緩存和 DRAM 都友好?SoA結構體數(shù)組拆成多個數(shù)組只更新位置時只讀位置數(shù)組不把無關字段搬進緩存?ECS / DOTS同類組件連續(xù)排列本質就是為內(nèi)存訪問模式設計的架構? 對象池復用連續(xù)內(nèi)存減少碎片和 GC前面對象池那一篇? 每幀遍歷大量GameObject再GetComponent對象分散在堆里到處跳著讀? 用LinkedList、大量class小對象指針追逐無法預取AoS常見寫法 [位置|血量|名字|AI狀態(tài)][位置|血量|名字|AI狀態(tài)]... 只想更新位置卻把整個對象都搬進緩存 SoA 位置 [位置][位置][位置][位置]... ← 每 64 字節(jié)都是有用數(shù)據(jù) 血量 [血量][血量][血量][血量]...前面講網(wǎng)絡時說往返次數(shù)比數(shù)據(jù)量更關鍵內(nèi)存也是一樣訪問 DRAM 的次數(shù)和方式往往比計算量更決定性能。十一、常見的誤解誤解實際情況“頻率越高延遲越低”頻率主要提升帶寬以納秒計的延遲變化不大要同時看 CL“讀一個字節(jié)就只傳一個字節(jié)”最少傳一整個緩存行64 字節(jié)“內(nèi)存訪問時間是固定的”行命中、行空閑、行沖突差好幾倍還受刷新和排隊影響“內(nèi)存是隨機訪問的所以順序無所謂”隨機訪問指能訪問任意地址不代表任意順序都一樣快“插滿內(nèi)存條一定更快”必須按主板說明組成雙通道才有帶寬加成Rank 多了有時還得降頻“DDR5 自帶 ECC等于服務器 ECC 內(nèi)存”O(jiān)n-die ECC 只保護芯片內(nèi)部傳輸路徑上的錯誤它管不到“內(nèi)存不夠就加性能問題就解決了”容量解決的是裝不下訪問模式不好再大的內(nèi)存也一樣慢一句話總結DRAM 海量電容按通道 → Rank → Bank → 行 → 列組織起來的倉庫。讀取要先激活一整行到行緩沖區(qū)再讀出需要的列換行前還要預充電關閉舊行電容會漏電所以每 64ms 要把所有行刷新一遍。它的性能特征可以概括為三句話帶寬很大延遲很高同一行里連續(xù)訪問最快。CPU 用緩存、預取和多 Bank 并行來掩蓋延遲而程序員能做的就是讓數(shù)據(jù)連續(xù)存放、順序訪問。