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(缓冲池) |
参考
- Computer Organization and Design, Patterson & Hennessy
- Agner Fog: CPU microarchitecture
- ClickHouse: Vectorized Execution