CPU 在数据库中的角色

数据库 90% 的热点时间花在 CPU 上执行:解析表达式、比较键值、做 Hash、解压数据。理解 CPU 的执行模型,是写出”缓存友好”与”向量化友好”代码的前提。

指令执行周期

CPU 执行每条指令大致经过四个阶段:

┌────────┐   ┌────────┐   ┌────────┐   ┌────────┐
│ 取指   │ → │ 译码   │ → │ 执行   │ → │ 写回   │
│ Fetch  │   │ Decode │   │ Execute│   │ Write  │
└────────┘   └────────┘   └────────┘   └────────┘
     ↑                                              │
     └──────────────── 下一条指令 ←────────────────┘

流水线 (Pipeline)

现代 CPU 不会等一条指令走完才取下一条,而是像工厂流水线一样重叠执行:

周期:  1   2   3   4   5   6
指令1: IF  ID  EX  WB
指令2:     IF  ID  EX  WB
指令3:         IF  ID  EX  WB

当流水线因数据依赖(下一条指令需要上一条的结果)或控制依赖(分支跳转)而停顿,性能骤降。数据库的表达式求值若充满分支(如 if (type == INT) ... else if (type == STR)),会频繁打断流水线。

多级缓存与缓存行

CPU 从内存取数据并非逐字节,而是以 缓存行 (Cache Line,通常 64 字节) 为单位整块加载。多级缓存构成金字塔:

        ┌─────────┐  速度最快 / 容量最小
  L1    │ 32-64KB │  每核私有, ~1ns
        ├─────────┤
  L2    │ 256KB-1MB│ 每核私有, ~4ns
        ├─────────┤
  L3    │  数十MB │  多核共享, ~10ns
        ├─────────┤
  内存   │  数十GB │  ~100ns
        └─────────┘  速度最慢 / 容量最大

局部性原理是性能关键:

  • 时间局部性:刚访问的数据很可能再次访问(循环变量)
  • 空间局部性:访问某地址后,附近地址也会被访问

数据库中的体现:

  • 顺序扫描比随机点查快,因为预取器能提前加载相邻缓存行
  • B+Tree 比二叉搜索树缓存友好:节点连续存储,一个缓存行装多个键

分支预测 (Branch Prediction)

CPU 会预测分支走向以提前执行。预测错误(mispredict)代价高昂(清空流水线,损失 ~10-20 周期)。

if (row.is_deleted) continue;   // 高可预测性:多数行未删除 → 预测命中
if (value > threshold) ...       // 随机分布 → 预测频繁失误

优化手段:少写数据依赖型分支,或用条件移动 (CMOV) 替代分支。列式引擎常把过滤做成”位图 + 批量跳过”,规避逐行分支。

SIMD 与向量化

现代 CPU 提供单指令多数据 (SIMD,如 AVX-512) 指令,一条指令并行处理 16/32/64 个数据:

标量:  for i: c[i] = a[i] + b[i]      // N 条指令
向量:  vadd(c, a, b)                   // 1 条指令处理 8 个 int32

这正是 向量化执行引擎(如 DuckDB、ClickHouse、PostgreSQL v17+ 的部分算子)比逐行 Volcano 模型快数倍的原因:一次比较 8 个值,且循环无分支。

多核与超线程

  • 多核:物理上多个执行单元,可真正并行处理不同查询或分区
  • 超线程 (SMT):一个物理核暴露两个逻辑核,在流水线空泡时切换线程填满资源

并行注意:多核共享 L3 与内存带宽,线程过多会因内存带宽瓶颈锁争用而下降(Amdahl 定律)。

对数据库设计的启示

硬件特性数据库应对
缓存行 64B紧凑行格式、对齐结构体
流水线怕分支向量化、位图过滤
SIMD批量列式计算
多核 + 带宽瓶颈并行扫描但控制线程数
缓存层次热点数据留 L3(缓冲池)

参考