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