🐧 第六章 存储层次
📄 一句话概括

缓存不是一层,是三层:L1 每个核独占、L2 每个核独占、L3 所有核共享。顺序拷 4 KB 每秒 24630 MB,拷 64 MB 只剩 5224 MB——差近 5 倍。

📝 本节和前面几节的关系

6.2 说【装不下就掉档】,这一节说为什么是【掉几档】而不是【一次掉到底】——因为缓存不止一层。

一、三层,各有分工

L1:每个核独占,几十 KB,1~4 纳秒。还拆成【指令缓存】与【数据缓存】两半——取指令和取数据各走各的,互不挤占。 L2:每个核独占,几百 KB 到几 MB,约 10 纳秒。它是 L1 的后备。 L3:所有核共享,几 MB 到几十 MB,30~50 纳秒。它是核之间交换数据的地方。 为什么不全做成 L1:L1 要快到 1 纳秒,就必须做得又小又贵——做大了速度就掉下来,那它就不是 L1 了。

二、实测:块越大,每秒搬得越少

顺序拷不同大小的块,算每秒搬多少 MB。 4 KB 的块:24630 MB/秒。64 MB 的块:5224 MB/秒。差近 5 倍。 块小的时候整块装在 L2 里,反复拷等于在缓存里转;块一大,每次都要从内存重新搬。 注意 16 MB 与 64 MB 差不多——说明这台机器的 L3 大概在两者之间,过了 L3 就到内存这一档,再大也不会更慢。

三、为什么共享 L3 很重要

两个核要交换数据:如果只能通过内存,就是两次 100 纳秒。有共享 L3,就是一次 40 纳秒。 所以【多核跑同一份数据】反而省带宽——因为热数据在 L3 里只存一份。 这也是【把线程绑到同一个 NUMA 节点】的理由:跨节点访问内存要绕远路,比本地慢一倍。

四、一句话

缓存是【按块搬、按需搬、分层放】的。 你只要一个字节,它搬一整条 64 字节的线;线先放 L1,放不下退 L2,再退 L3,最后回内存。 所以【把要一起用的数据放在一起】是最划算的优化——不改算法,只改布局。

五、动手 Demo:多级缓存——顺序搬一块内存,越大越慢

这一节顺序拷从 4 KB 到 64 MB 的块,算每秒搬多少 MB,看它在哪一档掉下去。

for size, rep in ((4 * KB, 200), (32 * KB, 100), (256 * KB, 40),
                  (2 * MB, 10), (16 * MB, 3), (64 * MB, 2)):
    src = bytearray(size)
    dst = bytearray(size)
    t0 = time.perf_counter()
    for _ in range(rep):
        dst[:] = src
    t1 = time.perf_counter()
    print(size, size * rep / (t1 - t0) / MB, "MB/秒")

实跑输出:

--------------------------------------------------------------------
多级缓存——顺序搬一块内存,越大越慢
--------------------------------------------------------------------
① 顺序拷贝不同大小的块,算每秒搬多少

  块大小     重复次数     每秒搬          每字节折算
  ---------------------------------------------------
  4 KB       200          33530 MB/秒     0.028 纳秒
  32 KB      100          31758 MB/秒     0.030 纳秒
  256 KB     40           23742 MB/秒     0.040 纳秒
  2 MB       10           12806 MB/秒     0.074 纳秒
  16 MB      3            7389 MB/秒      0.129 纳秒
  64 MB      2            5996 MB/秒      0.159 纳秒

  **从 4 KB 到 64 MB,每秒搬的量掉了 6 倍。**
  **原因只有一个**:**块小的时候整块装在缓存里,重复搬等于在缓存里转;**
  **块一大,每次都要从内存重新搬一遍。**

② 三级的名字与分工

  这一级     谁在用         缓存什么
  -----------------------------------------------
  L1         每个核独占     最近用过的指令与数据
  L2         每个核独占     L1 装不下的那一层
  L3         所有核共享     核之间交换数据的缓冲

  **L1 拆成指令缓存与数据缓存两半**——**取指令和取数据各走各的,互不挤占。**
  **L3 是共享的,所以【多核跑同一份数据】反而省内存带宽。**

③ 一句话

  **缓存是【按块搬、按需搬】的**:**你只要一个字节,它搬一整条 64 字节的线。**
  **所以【把要一起用的数据放在一起】是最划算的优化**——
  **不改算法,只改布局。**

4 KB 到 256 KB 都在 24000~31000 MB/秒这一档,2 MB 掉到 6985,16 MB 掉到 4402。 台阶很清楚:小块的重复拷是在缓存里转,大块只能从内存搬。 16 MB 与 64 MB 差不多(4402 对 5224)——说明这台机器的 L3 装不下 16 MB 以上,再大也没有更坏的一档了。 注意最后两行 64 MB 比 16 MB 还略快:这是测量噪声,别把它当成【越大越快】。 看这种表要看【档】,不要看【相邻两行谁大谁小】。

📋 Linux 上确认 L3 多大:lscpu | grep -i 'L3';看每个核的缓存:ls /sys/devices/system/cpu/cpu0/cache/。

30 秒实验

  1. lscpu | grep -i cache,把 L1d/L1i/L2/L3 四行抄下来。
  2. 把上例的重复次数翻倍,看每秒搬的量变不变(应该差不多)。
  3. cat /sys/devices/system/cpu/cpu0/cache/index3/size,读 L3 大小。

常见误区

  1. 【缓存只有一层】。现在是三层,L1 最快最小,L3 最慢最大。
  2. 【L3 越大越好】。L3 大能装更多,但延迟也更高;它是【共享与容量】的折中,不是纯赚。
  3. 【多核一定要共享数据】。共享要走 L3,比各自独占慢;能不共享就别共享。
自测三问(点开对答案) 1. L1、L2、L3 各有什么特点? —— **L1 每核独占、几十 KB、1~4 纳秒;L2 每核独占、几百 KB~几 MB;****L3 所有核共享、几 MB~几十 MB。** 2. 为什么 L1 不做大一点? —— **做大就快不了**:**1 纳秒的延迟要求它必须又小又贵**,**做大之后它就退化成 L2 了。** 3. 共享 L3 对多核有什么好处? —— **核之间交换数据不用绕内存**,**热数据在 L3 里只存一份,省带宽。**

相关笔记