CPU缓存基础:L1/L2/L3缓存的大小、延迟与带宽差异

做量化回测系统,说白了就是在跟时间赛跑。你想想看,一次回测可能要遍历几亿条行情数据,每笔交易都要计算、判断、记录。如果CPU每次取数据都要去内存里翻,那速度就太慢了。

这时候,缓存就派上了用场。我个人习惯把缓存理解成CPU的「随身笔记本」——常用的数据就记在上面,不用每次都去大书架上翻。

三级缓存:大小、延迟与带宽

现代CPU通常有三层缓存:L1、L2、L3。它们的大小、速度和位置都不一样。

缓存级别 典型大小 延迟(CPU周期) 带宽(估算) 位置
L1 32KB - 64KB(每核) 3-4 周期 ~1TB/s 每个核心独享
L2 256KB - 512KB(每核) 10-12 周期 ~500GB/s 每个核心独享
L3 8MB - 32MB(共享) 30-40 周期 ~200GB/s 所有核心共享

看到这个表格,你可能会问:为什么L1这么小?嗯,这里有个物理限制——速度越快,体积就越难做大。L1离CPU核心最近,走的是专用高速总线,所以延迟极低。但代价就是容量小得可怜,只有几十KB。

我在项目中遇到过一个问题:回测引擎里有个全局的「订单簿快照」结构体,大概200KB。一开始我把它放在一个全局变量里,结果每次访问都要从L3甚至内存里取。后来我把它拆成两个部分——热数据(经常访问的)和冷数据(偶尔访问的),热数据控制在48KB以内,刚好塞进L1。性能提升了将近30%。

核心原则:热数据越小越好,尽量塞进L1。如果塞不进,至少保证L2能装下。一旦溢出到L3或内存,性能会断崖式下跌。

缓存行(Cache Line):64字节的秘密

CPU从缓存里取数据,不是按字节取的,而是按「缓存行」取的。缓存行的大小通常是64字节。

什么意思呢?假设你只想要一个4字节的int,但CPU会一次性把包含这个int的连续64字节都加载进来。这就像你去图书馆借一本书,管理员直接把整个书架层都搬过来了。

为什么会这样?因为程序访问内存有个规律——空间局部性。你访问了地址A,很可能接下来会访问A+1、A+2……一次性加载64字节,可以大幅减少访问次数。

但这也带来了一个问题:伪共享(False Sharing)。我曾经在优化一个多线程回测引擎时踩过这个坑。两个线程各自修改不同的变量,但这两个变量恰好落在同一个缓存行里。结果呢?每次修改都会导致整个缓存行失效,两个线程互相「打架」,性能直接腰斩。

避坑指南:多线程环境下,如果两个线程频繁修改的数据在同一个缓存行里,性能会急剧下降。我曾经因为这个bug排查了整整两天,最后用perf发现缓存未命中率高达80%。解决方案很简单:用内存对齐(alignas(64))把热数据隔开,或者用填充字节让它们落在不同的缓存行里。

缓存一致性协议:MESI

多核CPU里,每个核心都有自己的L1/L2缓存。问题来了:如果核心A修改了某个变量,核心B怎么知道这个变量已经变了?

这就是缓存一致性协议要解决的问题。最常见的协议叫MESI,它给每个缓存行打了四个状态标签:

  • M(Modified,已修改):这个缓存行被当前核心修改了,其他核心的副本都失效了。
  • E(Exclusive,独占):只有当前核心有这个缓存行,而且数据跟内存一致。
  • S(Shared,共享):多个核心都有这个缓存行,数据跟内存一致。
  • I(Invalid,无效):这个缓存行已经失效了,需要重新加载。

状态切换的规则其实不复杂。我简单说几个关键场景:

  • 核心A读一个变量,如果缓存行是I状态,就去内存加载,变成E状态。
  • 核心B也来读同一个变量,核心A的缓存行从E变成S,核心B的缓存行也是S。
  • 核心A要修改这个变量,如果当前是S状态,它必须先通知核心B:「我要改了,你的副本作废」。然后核心A变成M状态,核心B变成I状态。

这个通知过程叫「总线嗅探」。说白了就是核心之间通过总线广播消息:「谁有X变量的副本?我要改了!」

个人经验:在回测系统里,如果你用多线程并行处理不同的股票,尽量让每个线程处理的数据完全独立——不同的股票代码、不同的时间段。这样缓存行就不会被多个核心共享,MESI协议的状态切换开销就降到了最低。我习惯把数据按股票代码做分区,每个线程只处理一个分区,效果很好。

知识体系总览

下面这张图概括了本章的核心内容:

CPU缓存知识体系 三级缓存 L1:32-64KB,3-4周期 L2:256-512KB,10-12周期 L3:8-32MB,30-40周期 速度越快,容量越小 热数据尽量塞进L1 缓存行(Cache Line) 大小:64字节 一次性加载连续数据 利用空间局部性 ⚠ 伪共享陷阱 多线程修改同一缓存行 → 性能断崖下跌 MESI协议 M:已修改 E:独占 S:共享 I:无效 总线嗅探机制 数据分区避免共享 核心目标:减少缓存未命中 → 提高数据局部性 → 避免伪共享

这张图把三个核心知识点串在了一起。你从左往右看:先理解三级缓存的层次结构,再理解缓存行怎么加载数据,最后理解多核之间怎么通过MESI协议保持数据一致。这三者环环相扣,任何一个环节出了问题,回测系统的性能都会受影响。

好了,这一章的内容就到这里。记住一句话:缓存不是无限的,但优化缓存利用率的收益是无限的。下一章我们会深入讨论如何利用这些知识来优化回测系统的内存布局。