你的电脑内存,其实是一场精心维护的谎言

从缓存行、虚拟地址到 NUMA、压缩内存与 SSD,逐层拆解现代计算机内存层级中那些被抽象隐藏的真相。

刚开始学编程的时候,内存看起来非常直白:一堆字节,每个都有自己的地址。想读或写,就给出地址,计算机便跑到那个位置替你办事。虽然部分地址可能留给硬件或操作系统,但核心模型很简单——内存就是一个巨大的字节数组。

就像我们把导线当作理想导体一样,这个“大数组”模型在从前只是对真相的一点轻微扭曲。到了现代计算机上,它几乎完全是个谎言。你使用的地址很可能不是物理地址;你读到的数据可能压根不来自内存条;即使来自内存,它也可能躺在某一级缓存里。顺着这条线索一路挖下去,你会发现 SSD 大概也只是内存层级中的另一层而已。

速度差距逼出的缓存

根本矛盾在于:处理器比内存快太多了。现代处理器每个时钟周期能执行好几条指令,而访问普通 DRAM 却可能要几百个周期。如果每次取数据都要等内存,那么绝大部分算力都会被白白浪费。

解决办法就是缓存:把常用数据的副本放在更小、更快、离处理器更近的存储器里。现代桌面处理器通常至少有三层缓存。L1 容量极小但极快,往往还拆成指令缓存和数据缓存;L2 更大一些、稍慢;L3 更大,通常也更慢。

多核时代让情况变得有趣。通常每个核心有自己独享的 L1 和 L2,而若干核心共享 L3。但这并非铁律:有些处理器让多个核心共享 L2,也有些处理器存在多个 L3,各自服务一组核心。具体怎么排布,取决于处理器架构。

一个关键细节是:缓存并不是逐字节工作的。它以“缓存行”为单位搬运和追踪数据,现代桌面处理器上常见的大小是 64 字节。如果你读取一个尚未被缓存的字节,处理器通常会把包含它的整条缓存行一起取回来。

这就是顺序访问往往比随机访问快得多的原因——当你读下一个字节时,它很可能已经在缓存里了,硬件预取器甚至会提前猜你接下来要什么。

但问题也随之而来。设想两个核心在修改两个毫不相干的变量:如果这两个变量恰好落在同一条缓存行上,两个核心就可能不断让对方手里的副本失效。它们逻辑上并没有共享数据,却在实实在在地浪费时间。

MESI 之类的协议负责维持缓存一致性,协调某条缓存行归谁所有、哪些副本仍然有效。MESI 是缓存行四种状态的缩写:已修改(Modified)、独占(Exclusive)、共享(Shared)、无效(Invalid)。内存系统必须想办法让缓存保持同步。但一致性并不能取代多线程程序里的同步机制和内存序规则。重点在于:连我们以为简单的“按字节寻址的内存”,背地里也是以大块为单位运作的。

地址同样不是真的

到目前为止,我们默认你提供的地址指向内存条上的某个位置。通常并非如此。现代操作系统使用虚拟内存,程序看到的是一个虚拟地址空间,由操作系统和处理器协作映射到物理内存上。

内存管理单元(MMU)通过页表把虚拟地址翻译成物理地址。这让不同程序可以使用完全相同的虚拟地址而互不干扰,同时提供了内存保护,也让物理页可以被共享。当然,翻译地址需要时间,于是我们又加了一层缓存。

转译后备缓冲器(TLB)会记住最近用过的地址翻译结果。TLB 命中就能避免一次可能很昂贵的页表遍历,有些处理器甚至有多级 TLB。与按缓存行运作的普通 CPU 缓存不同,虚拟内存以页为单位,常见大小是 4 KB,也支持更大的页。

注意这里发生了什么:我们有存数据的缓存、存指令的缓存,还有存“去哪里找数据和指令”这类信息的缓存。读一个变量这么简单的动作,已经变得出奇地复杂。

内存条之间也不平等

就算穿过所有缓存、终于抵达内存,事情也未必简单。在多处理器或某些多核布局的系统上,内存访问可能是不均匀的,这就是 NUMA,即非统一内存访问。

处理器访问挂在自己 NUMA 节点上的内存相对较快;访问另一个节点的内存则要跨过互连通道。两处都是普通内存,可能还处在同一个地址空间里,但访问延迟和可用带宽并不相同。

操作系统会尽量把内存放在使用它的处理器附近。因此,把一个线程迁移到另一个 CPU 上,即便程序里的地址一个都没变,它的内存性能也可能随之改变。

NUMA 也不只存在于传统多路服务器上。采用小芯片(chiplet)设计、异构内存以及 CXL 等技术的现代系统,正在让内存拓扑变得越来越有看头。

数据可能根本不在内存里

虚拟内存还有另一个戏法:一个虚拟页在你访问它的那一刻,不一定非得由物理内存支撑。

当程序访问的页面当前没有以可用的方式映射时,处理器会产生页错误,由操作系统决定怎么办。有时它只是新分配一个页;有时它从可执行文件或内存映射文件中把数据取回来;还有时,该页早已被换出到交换空间,需要重新读回。值得注意的是,可执行文件之类的内容通常是不可变的,所以大多数操作系统不会把它们换出去,而是直接丢弃,反正需要时还能重新载入同一页。

这意味着,你的程序可以寻址到当前并不在物理内存里的数据。访问 SSD 自然比访问内存慢得多,所以过度换页能让整台机器几乎瘫痪。不过虚拟内存本身并不依赖换页:即使系统从不把页面写入磁盘,地址翻译与内存保护依然有其价值。

SSD 也是内存的一层

假设你的程序读取一个文件。你可能会想,操作系统会向 SSD 发请求,然后等数据回来。但如果这个文件最近被访问过,数据可能已经在内存里了。

操作系统维护着文件系统页缓存,把频繁或最近访问过的文件数据留在内存中;写入操作也可能先在内存里缓冲,之后才写入持久存储。

于是,内存就成了二级存储的缓存,正如 L3 是内存的缓存一样。这个类比并不完美:内存同时也是运行程序的主要工作区,匿名内存未必是磁盘上任何东西的副本。但整体层级是清晰的——寄存器、CPU 缓存、内存、持久存储,在速度、容量和成本之间提供了层层不同的取舍。

连 SSD 内部也有自己的缓存和地址翻译。闪存翻译层把逻辑块地址映射到实际的闪存位置,把磨损均衡和垃圾回收对主机隐藏起来。看来关于地址的真相,谁都不太想说实话。

把中间那层压缩起来

但如果能在不加内存条的前提下,让内存“变大”呢?压缩提供了另一种把内存层级搞复杂的方法。与其把不常用的页立刻搬到磁盘,为什么不把它压缩后留在内存里?

Linux 有两个知名机制:zram 和 zswap。zram 在内存中创建一个压缩块设备,常被用作交换设备,让页面以压缩形式存放,而不是写进物理存储。zswap 则是位于交换设备前面的压缩缓存:当 Linux 决定换出某页时,zswap 会尝试压缩并把它留在内存中;必要时,这些页最终仍可能被逐出到后备交换设备。

两者都是用处理器时间换取内存容量、并减少存储 I/O。解压一页要消耗 CPU 周期,但比起从 SSD 取回,往往还是便宜得多。

不过有个前提:这些机制都走交换子系统。当被压缩的页再次被需要时,必须先解压成一个普通页,程序才能访问。这意味着页错误、内存管理开销和数据搬移。那么,如果压缩内存能表现得像普通内存呢?

Meta 想改写规则

这正是 CRAM(Compressed RAM,压缩内存)的思路。该项目由 Meta 的 Gregory Price 在 2026 年 10 月的 Linux Plumbers 大会上介绍,Phoronix 也有相关报道。

CRAM 不把压缩内存当作传统的交换设备,而是通过一个特殊的 NUMA 节点把它暴露出来。这样 Linux 就能复用现有的内存管理机制,在普通 DRAM 与压缩内存之间搬动页面。最有趣的部分是硬件辅助的压缩与解压缩,它让访问粒度比传统按页压缩的交换机制细得多。

开发者报告的基准测试成绩相当亮眼:某些测试中读取性能接近普通 DRAM,在选定工作负载上相对 zram 有显著提升。

但这并不意味着压缩突然变得免费。写入、压缩比、硬件要求以及不可压缩数据的处理,都仍是问题。CRAM 目前仍处于开发阶段,还不能日常使用。

不过它揭示了内存层级的走向:操作系统不再只是判断某一页在内存里还是在磁盘上,而可能在普通内存、压缩内存、更慢的内存层级和二级存储之间做选择。

幻觉本身就是功能

大多数程序员在大多数时候不需要考虑这些,这正是抽象存在的意义。但一旦性能变得重要,幻觉就会开始漏风:顺序访问可能比随机访问快出巨大差距;两个线程修改无关变量也可能为缓存行打架;进程在 CPU 之间迁移会改变内存延迟;一个看起来普通的内存页,背后可能要做一次解压,或者跑一趟 SSD。

当然,在较小的嵌入式系统上,画面可能完全不同。没有 MMU、也没有复杂缓存时,内存看起来就非常接近我们最初学到的那个简单物理地址空间。

我们从“一个字节数组”出发,最后得到的是缓存层级、翻译表、NUMA 节点、压缩内存池和持久存储设备——它们通力合作,只为维持那个简单的幻觉。一件事能复杂到这个地步,也真是令人感叹。

收录于

讨论

0 条评论

同分类推荐

硬件