计算机组成原理
存储层次金字塔
| 层次 | 典型容量 | 延迟量级 | 带宽 | 谁管理 |
|---|---|---|---|---|
| 寄存器 | ~KB(每 core 几十个) | 1 cycle | — | 编译器 |
| L1 Cache(I/D 分离) | 32KB + 32KB / core | ~4 cycles | >1TB/s | 硬件 |
| L2 Cache | 256KB–2MB / core | ~12 cycles | ~500GB/s | 硬件 |
| L3 Cache(LLC) | 数 MB–数百 MB / socket | ~40 cycles | ~100GB/s | 硬件,多 core 共享 |
| 本地 DRAM | 几十 GB–几 TB / socket | ~100ns(~300 cycles) | ~50–100GB/s(DDR5) | OS(虚拟内存) |
| 远端 NUMA DRAM | 其他 socket | ~200ns(1.5–2x) | ~30–50% 本地带宽 | OS + 硬件互联 |
关键规律
- 越靠近 CPU:容量越小、延迟越低、带宽越高、成本/bit 越高
- 延迟差 100x:L1(1ns)到 DRAM(~100ns),相当于从书桌到楼下超市取东西
- 带宽差 20x:L1(>1TB/s)到 DRAM(~50GB/s)
- NUMA 惩罚:跨 socket 访问延迟增加 50–100%,带宽下降 30–50%
可能的原因(从硬件到系统):
- Cache 污染:多线程共享 L3,工作集挤掉彼此的热数据
- False sharing:独立变量落在同一 cache line(见 Cache/TLB 章节)
- 内存带宽饱和:所有 core 同时打 DRAM,带宽成为瓶颈
- NUMA 远端访问:线程被调度到远端 socket
- 锁竞争 / 上下文切换:软件层面串行化
几个快速判断方法:
perf stat看 IPC(instructions per cycle):高 IPC(>1)说明算力用满了;低 IPC(<0.5)通常是访存/分支等待- Cache miss rate:L1/L2/LLC miss 高 → 访存瓶颈
- Roofline 模型:计算 Operational Intensity(FLOPs/Byte),看落在算力拐点左侧还是右侧(见性能预测 / Roofline 章节)
- 缩数据实验:把工作集缩到 L3 以内,如果吞吐飙升 → 原先是 DRAM 带宽瓶颈
Cache 工作机制
| 概念 | 核心含义 | 面试关键词 |
|---|---|---|
| Cache Line | 缓存的最小单位,x86 典型 64 字节 | 64B、spatial locality、对齐 |
| 映射方式 | 直接映射 / N 路组相联 / 全相联 | 现代 CPU 多为 N-way set associative |
| 写策略 | Write-through(直写)vs Write-back(回写) | 现代 L1/L2/L3 多用 write-back + write-allocate |
| 包含策略 | Inclusive(L3 包含 L2)vs Exclusive / NINE | Intel 多用 inclusive,AMD Zen 多用 non-inclusive |
| 时间局部性 | 刚访问的数据很快再次访问 | 循环变量、热路径代码 |
| 空间局部性 | 相邻地址的数据很快被访问 | 数组顺序遍历比链表快 |
TLB:地址翻译缓存
- 作用:缓存虚拟页→物理页帧(PFN)映射,避免每次访存都查多级页表
- 层级:L1 ITLB/DTLB(每 core,小而快)→ L2 TLB(shared,更大)
- Page size:4KB 常规页;2MB/1GB 大页(HugePage)减少 TLB entry 数量
- TLB Miss 代价:页表遍历(page walk)需多次访存(x86 4级页表≈4次DRMA访问),~100–200 cycles
False Sharing(伪共享)
现象:多线程修改逻辑独立的变量,但它们落在同一 64B cache line 上 → cache coherence 协议在 core 间反复 invalidate + transfer → 实际变成串行访问。
检测:perf c2c(cache-to-cache)定位热点 cache line。
解决:
- Padding:在变量间填充字节,让它们落在不同 cache line
alignas(64):C++11 标准属性按 cache line 对齐- 线程本地分片:每线程独立计数,最后汇总
- 减少共享写:共享读无问题,共享写才会触发 coherence traffic
写友好代码的原则
| 原则 | 做法 | 反例 |
|---|---|---|
| 顺序访问 | 数组行优先遍历 | 列优先遍历(cache miss 暴增) |
| 数据对齐 | 结构体按 cache line 对齐热点字段 | 跨 cache line 的未对齐访问 |
| 减小工作集 | 分块(tiling/blocking)处理大矩阵 | 对整个大矩阵随机访问 |
| 用大页 | 2MB HugePage 减少 TLB miss | 随机访问大内存用 4KB 页 |
| 避免 false sharing | Padding + thread-local | 多个线程写相邻的计数器 |
数组元素在内存中连续存放,遍历时 cache 每次 miss 会预取后续 64B(一个 cache line 含 ~8 个 8 字节元素),空间局部性好。链表节点分散在堆中,每次访问几乎都 cache miss,还可能造成 TLB miss。实测差距可达 10–50x。
以 2MB 大页 vs 4KB 常规页为例:2MB = 512 × 4KB,同样的虚拟地址范围需要的 TLB entry 数减少到 1/512。大内存随机访问场景(如大模型 KV cache、数据库 buffer pool)TLB miss 率显著下降。1GB 大页更激进,但需预留连续物理内存,适合长期运行的大进程。
DMA、PCIe 与 NUMA 拓扑
DMA 允许设备绕过 CPU 直接读写内存;PCIe 是 CPU、GPU、NIC、NVMe 等设备的主要互联;NUMA 决定 CPU、内存、GPU、NIC 之间的亲和关系。
AI Infra 为什么关心这些
| 概念 | 影响 | 典型场景 |
|---|---|---|
| DMA | 降低 CPU copy 开销 | GPU copy、RDMA、NVMe 数据加载 |
| PCIe | 限制 host-device 带宽 | CPU 到 GPU 数据搬运 |
| NUMA locality | 影响 CPU-GPU/NIC 距离 | 数据加载线程应靠近目标 GPU/NIC |
| GPU-NIC affinity | 影响 RDMA/NCCL 性能 | 跨节点 AllReduce |
设备路径怎么读
在一台多 Socket 服务器里,GPU、NIC、NVMe 通常挂在不同 PCIe switch 或 root complex 下。路径越短、越少跨 Socket,延迟越低、带宽越稳定。AI Infra 里常见的性能问题不是“GPU 不够快”,而是数据从 CPU、NIC 或另一张 GPU 到目标 GPU 的路径太差。
nvidia-smi topo -m?它能显示 GPU-GPU、GPU-NIC、GPU-CPU 的拓扑关系。张量并行、NCCL、RDMA 和数据加载都受拓扑影响;同样 8 张 GPU,NVLink 内互联和跨 PCIe/跨节点性能差异很大。
指令集基础
| 维度 | CISC(x86) | RISC(ARM/RISC-V) |
|---|---|---|
| 设计理念 | 复杂指令,一条指令做更多事 | 简单定长指令,硬件高效执行 |
| 指令长度 | 变长(1–15 字节) | 定长(ARM 32bit / Thumb 16bit) |
| 寄存器数量 | 16 个 GPR(x86-64) | 32 个 GPR(ARM64/RISC-V) |
| 代表 | Intel/AMD 服务器/桌面 | 手机/嵌入式/Apple M系列/昇腾 |
| AI Infra 场景 | 主流训练/推理服务器 | 边缘推理、Apple Silicon、自研芯片 |
流水线(Pipelining)
- 经典 5 级流水线:取指(IF)→ 译码(ID)→ 执行(EX)→ 访存(MEM)→ 写回(WB)
- 理想 CPI = 1:每个 cycle 完成一条指令(但实际被冒险打断)
- 三类冒险:
- 结构冒险:硬件资源冲突(如同一周期争用 ALU)→ 增加硬件资源/流水线停顿
- 数据冒险:指令间依赖(RAW/WAR/WAW)→ 旁路(forwarding/bypass)、流水线停顿
- 控制冒险:分支/跳转目标不确定 → 分支预测、延迟槽、预取
- 超流水线:级数更深(如 Pentium 4 有 31 级),提升频率但分支误判代价更高
- 超标量(Superscalar):每 cycle 发射多条指令到多个执行单元,现代 x86 可 4–6 发射
- 乱序执行(OoO):不按程序序发射,按数据就绪顺序执行,隐藏延迟(ROB、保留站)
分支预测(Branch Prediction)
- 为什么重要:误判一次分支意味着清空流水线,代价 10–20 cycles
- 静态预测:编译器提示(如 __builtin_expect)、总是向后跳转(循环)
- 动态预测:
- 1-bit / 2-bit 饱和计数器:记录分支历史方向
- (GShare/Perceptron):结合全局/局部历史,现代预测准确率 >95%
- BTB(Branch Target Buffer):预测跳转目标地址
- RAS(Return Address Stack):预测函数返回地址
- AI Infra 启发:写 if-else 时让高频路径走进分支(如常见输入走 then),减少误判;unlikely/likely 宏提示编译器
SIMD:单指令多数据
一条指令同时处理多个数据元素,是 CPU 上向量化计算的基础。
| 指令集 | 架构 | 宽度 | 典型用途 |
|---|---|---|---|
| SSE / SSE2/3/4 | x86 | 128 bit(4×float 或 2×double) | 早期向量化 |
| AVX / AVX2 | x86 | 256 bit(8×float) | 主流 CPU GEMM 内核 |
| AVX-512 | x86(Xeon) | 512 bit(16×float) | HPC、部分推理场景 |
| NEON | ARM/ARM64 | 128 bit | 移动端/边缘推理 |
| SVE / SVE2 | ARMv8/v9 | 可变长度(128–2048 bit) | HPC、富岳/AWS Graviton |
| AMX(Advanced Matrix Extensions) | Intel Sapphire Rapids+ | tile 矩阵运算 | CPU 上的矩阵乘加速 |
GPU 对应:CUDA 的 warp 单指令多线程(SIMT)本质是 SIMD 的扩展;Tensor Core 做的是矩阵 tile 运算,和 CPU AMX 同类。
三个原因:(1) 减少循环控制指令(比较+跳转)的比例,分支预测压力小;(2) 展开后指令调度器能看到更多独立指令,填充流水线发射槽,提升 IPC;(3) 给编译器更多机会做 SIMD 向量化。但过度展开会增加代码 size,导致 I-cache 压力。
现代 OoO 处理器流水线深 10–20 级,误判需清空流水线重新取指,代价约 10–20 cycles。高频 if-else 中误判率 20% 就会让 IPC 大幅下降。优化方法:
- 减少分支:用条件移动(CMOV)替代短 if-else
- 排序数据:先按条件排序让同一分支集中(如 partition 数据)
- likely/unlikely 宏:给编译器静态预测提示
- 查表/分支表:switch 跳转表替代多分支
- 向量化:SIMD 用 blend/mask 指令无分支处理
Cache Coherence:MESI 协议
每个 cache line 处于四种状态之一:
| 状态 | 全称 | 含义 |
|---|---|---|
| Modified | 已修改 | 本 core 独有,且被修改(脏),写回前其他 core 不能读 |
| Exclusive | 独占 | 本 core 独有,且和内存一致(干净) |
| Shared | 共享 | 多个 core 都有副本,和内存一致(干净) |
| Invalid | 无效 | 该 cache line 无效/不存在 |
- 读请求:如果本地是 I,向总线发 Read,其他 core 或内存响应;根据是否有其他 core 持有进入 S 或 E
- 写请求:必须先获得所有权(Read For Ownership),向其他 core 发 Invalidate,其他 core 置 I;写后进入 M
- 核间通信:通过 invalidate/response 消息(Ring Bus / Mesh 网络)维护一致性
MESIF(Intel)/ MOESI(AMD)在 MESI 基础上增加 F(Forward)/ O(Owned)优化共享数据转发。
伪共享与 Coherence Traffic
- 多 core 同时写同一 cache line → 反复 Invalidate + 重新获取所有权 → cache line 在 core 间"弹跳"
- 现象:CPU 利用率高但吞吐上不去
- perf 观测:
perf stat -e cache-misses,mem_load_retired.l3_miss,或perf c2c(cache-to-cache)
内存一致性模型
Cache coherence 保证"所有 core 最终看到相同的值",但不保证"看到的顺序"。
| 模型 | 含义 | 代表架构 |
|---|---|---|
| SC(Sequential Consistency) | 所有 core 的读写像按某个全局顺序执行 | 理想模型,无硬件实现 |
| TSO(Total Store Order) | Store 按序、Store→Load 可能重排(允许 Store Buffer) | x86(最强内存模型) |
| 弱内存模型(Weak/Relaxed) | Load/Store 可随意重排,需显式屏障 | ARM、RISC-V、PowerPC、GPU(PTX) |
x86-TSO 下的重排规则:本质上只有 StoreLoad 重排(写后读可能越过写),其他三种(LoadLoad、LoadStore、StoreStore)都是保序的。所以 x86 只需要 mfence(或 lock 前缀指令)作为完整屏障。
内存屏障与 C++ Atomic
| 屏障 | 作用 | 典型场景 |
|---|---|---|
| Write memory barrier(smp_wmb) | 保证屏障前的写先于屏障后的写 | 发布数据(先写数据,再写 ready 标志) |
| Read memory barrier(smp_rmb) | 保证屏障前的读先于屏障后的读 | 依赖读取(先读 ready 标志,再读数据) |
| Full memory barrier(smp_mb) | 双向全屏障 | StoreLoad 重排防护 |
C++ std::atomic 提供 6 种 memory order:
memory_order_seq_cst:顺序一致(最强,默认)memory_order_acq_rel:Acquire-Release(用于 CAS)memory_order_acquire:读侧屏障(后续读不越过)memory_order_release:写侧屏障(前面写不越过)memory_order_consume:数据依赖序(极少使用,多数编译器升级为 acquire)memory_order_relaxed:无顺序保证(只保证原子性),用于计数器
不能完全解决。MESI 保证 cache 一致性(最终所有 core 看到一致值),但存在两个问题:(1) Store Buffer:core 写数据先放入 store buffer,立即继续执行后续指令,其他 core 此时看不到新值——这是 x86 TSO 允许 StoreLoad 重排的根源;(2) Load Buffer:乱序执行让读操作可能提前执行。volatile 只保证编译器不优化掉访问(不重排、不缓存到寄存器),但不插入 CPU 内存屏障;atomic 同时约束编译器和 CPU。
x86 是 TSO(强内存模型),硬件帮你保证了 LoadLoad/LoadStore/StoreStore 顺序,只有 StoreLoad 一种重排,很多"错误"的代码在 x86 上碰巧能跑。ARM/RISC-V 是弱内存模型,四种重排都可能发生,必须用正确的 memory order(acquire/release)才能保证正确性。C++ atomic 如果都用 seq_cst 是可移植的,但性能差;精细调优时要用对 acq/rel。
I/O 方式演进
| 方式 | 原理 | CPU 参与 | 适用场景 |
|---|---|---|---|
| 轮询(PIO / Programmed I/O) | CPU 不断读设备状态寄存器 | 极高(CPU 100% 忙等) | 早期简单设备、低延迟专用场景(DPDK 轮询模式) |
| 中断驱动 I/O | 设备准备好后发中断,CPU 响应处理 | 低(等待时 CPU 可做别的事) | 键盘、磁盘、低吞吐网络 |
| DMA | DMA 控制器(或设备自身作为 bus master)直接在设备和内存间搬数据 | 仅启动和收尾(初始化 DMA 描述符) | 网卡、GPU、SSD、高性能存储 |
DMA 工作流程(以网卡收包为例)
- 初始化:驱动在内存中分配环形缓冲区(RX ring),将 buffer 物理地址写入网卡 DMA 描述符
- 收包:网卡收到数据包,通过 DMA 直接写入 RX ring 的 buffer(不需 CPU 参与)
- 通知:网卡发中断(或 NAPI 轮询模式下中断触发后轮询)通知 CPU 有包可处理
- 处理:CPU(内核协议栈 / DPDK 用户态轮询)读取 buffer 处理数据
- 归还:buffer 处理完后还给驱动,重新挂到 RX ring
GPU DMA:GPU 通过 PCIe 直接访问 Host 内存(cudaMemcpy 等),GPUDirect P2P 允许 GPU↔GPU 或 GPU↔NIC 直接传输不经过 Host 内存。
中断与上下文切换
- 中断处理分两部分:
- 上半部(HardIRQ):立即响应,关中断,做最紧急的事(如从 NIC 取数据),必须快
- 下半部(SoftIRQ / tasklet / workqueue):开中断,延迟处理重活(如协议栈解析)
- 中断亲和性(IRQ Affinity):将中断绑定到特定 CPU core,避免跨核 cache miss;高性能场景下每个 RX 队列中断绑一个 core(RSS + RPS/RFS)
- 中断风暴:高吞吐下中断过于频繁,CPU 全在处理中断来不及干活 → NAPI(New API):中断触发后切换到轮询,一次性处理多个包后再开中断
- DPDK/SPDK:完全绕过内核中断,用户态轮询驱动(PMD),零中断零拷贝,极低延迟但占满 CPU
Hardware Prefetching(硬件预取)
CPU 检测内存访问模式,提前把数据加载到 cache,减少 cache miss。
- Stream prefetcher:检测顺序访问(步长固定),向前预取多个 cache line(最常见)
- Stride prefetcher:检测固定步长(如每 4 个元素访问一个)
- NL prefetcher(Next-line):总是预取下一个 cache line
- IP-based / Delta prefetcher:按 PC 历史记录预取
- 局限性:预取只对规则访问有效;随机访问(链表、哈希表)无法预取;预取过度会污染 cache
- 软件预取:
__builtin_prefetch(&x)/PREFETCHT0指令手动提示 CPU 提前加载,对不规则但可预测的访问有帮助(如 B-Tree 遍历)
线速 100Gbps 下,一个 64 字节小包每 6.7ns 就到达一个,中断频率 ~148Mpps,中断处理开销(上下文切换 + cache miss + 中断路由)远超轮询。DPDK 采用用户态 PMD 轮询模式:(1) 绕过内核,零 syscall 开销;(2) 大页 + 物理地址连续内存减少 TLB miss;(3) 每个 core 独占一个 RX 队列,无锁;(4) 轮询模式没有中断延迟。代价是 CPU 100% 占用,但在低延迟/高吞吐场景这是可接受的 tradeoff。
在多路服务器上,PCIe 插槽直接挂在特定 socket(NUMA node)上。GPU 和 NIC 如果在同一个 socket 上,它们之间 DMA 通过本地 PCIe root complex,延迟低、带宽高;跨 socket 则必须走 UPI/QPI 链路,延迟增加、带宽受互联带宽限制。AI 训练多机多卡场景中:GPU 直接接在 CPU0 socket 的 PCIe 上,NIC 最好也插在 CPU0 上,GPUDirect RDMA 才能走最短路径。nvidia-smi topo -m 和 lstopo 可以查看拓扑。