计算机体系结构:CPU、内存与 GPU / Computer Architecture: CPUs, Memory, and GPUs
性能首先是数据流问题。处理器再快,也无法弥补数据未及时到达、任务不够并行或同步过多。
体系结构学习的目标不是背诵某一代 CPU 或 GPU 的参数,而是理解程序语义如何映射为指令、数据移动和等待。源代码中的一次数组访问,可能命中寄存器或 Cache,也可能触发跨 NUMA 节点访问;一个看似并行的循环,可能因为依赖链、分支或内存带宽而无法利用全部执行单元。
从指令到数据的执行路径
程序指令
-> 前端取指、译码与分支预测
-> 调度到标量、向量或专用执行单元
-> 访问寄存器和多级 Cache
-> 未命中时访问主存或远端 NUMA 内存
GPU Kernel
-> Grid / Block / Warp 调度
-> 大量线程隐藏等待
-> 合并访问设备内存
-> 必要时经互连与主机交换数据CPU 用复杂控制、乱序执行和较大的缓存降低单线程延迟,适合分支多、任务不规则或串行比例较高的工作。GPU 用大量较简单的执行通道提高吞吐,适合规则、数据并行且计算量足以覆盖传输与启动成本的工作。二者不是简单的快慢关系,选择取决于依赖结构、数据规模、访问规律和端到端成本。
核心问题
| 问题 | 首先检查 |
|---|---|
| CPU 为什么没有跑满? | 分支、依赖、Cache miss、线程竞争 |
| GPU 为什么不快? | 并行度、访存合并、占用率、CPU-GPU 传输 |
| 多核为何不扩展? | 串行区、NUMA、共享数据和负载不均 |
表中的指标只是调查入口。例如 CPU 使用率低可能在等待 I/O,也可能线程被锁阻塞;GPU Occupancy 高不代表访存有效;Cache miss 多是否重要,还取决于处理器能否用其他独立工作隐藏延迟。必须把硬件计数器与源代码调用路径、工作负载和耗时联系起来。
内存层次决定可供给的性能
处理能力的增长通常快于外部内存带宽,因此数据复用和局部性往往比减少几条算术指令更重要。连续遍历有利于预取与缓存行利用,结构体数组和数组结构体会形成不同的向量化与访存行为。多核共享数据还会引入一致性流量;即使线程修改不同变量,只要它们落在同一缓存行,也可能发生伪共享。
NUMA 系统中,页面通常在首次访问时决定物理位置。线程迁移到其他节点后访问这批页面,会产生额外延迟和互连流量。并行程序不仅要把计算分给核心,也要让线程靠近自己的数据,并控制跨节点共享。
指令集与微架构
指令集架构定义软件可见契约,例如寄存器、指令、内存模型和异常。 微架构决定处理器如何实现该契约。
同一 ISA 可以有不同流水线、Cache、执行宽度和功耗特征。 因此“支持相同指令”不代表性能相同。
ISA contract
-> fetch and decode
-> rename and schedule
-> execute
-> retire in architectural order乱序执行允许处理器在等待某条指令时执行其他独立工作。 最终退休仍保持架构要求的可观察顺序。
CPU 前端
前端负责取指、分支预测、译码和向后端提供微操作。
前端瓶颈可能来自:
- 指令 Cache miss;
- iTLB miss;
- 分支预测错误;
- 代码体积;
- 译码带宽;
- 微操作 Cache;
- 间接跳转;
- 频繁虚调用。
大型展开可能减少循环控制,却增加代码体积和前端压力。 优化需要测量,不能假设展开越多越快。
分支预测
条件分支的真实方向通常在流水线后部才能确定。 处理器预测方向并沿预测路径执行。
预测错误时,错误路径工作被丢弃并重新取指。 代价与流水线深度和可用独立工作有关。
数据相关且近似随机的分支难以预测。 可以考虑排序、分区、查表或无分支写法,但替代方案也可能增加指令和内存访问。
依赖与指令级并行
真实数据依赖形成关键链:
a = load(p)
b = f(a)
c = g(b)
d = h(c)后一步必须等待前一步结果。 增加执行单元无法突破这条依赖链。
多个独立累加器、批量处理和算法重构可以暴露更多独立工作。 编译器是否成功调度,需要结合汇编和计数器检查。
标量与向量执行
SIMD 指令一次处理多个数据元素。
向量化收益取决于:
- 数据连续性;
- 对齐;
- 循环依赖;
- 指针别名;
- 分支和 Mask;
- Gather/Scatter;
- 数据类型;
- 向量宽度;
- 频率变化。
更宽向量不保证线性加速。 内存带宽、尾部处理和降频都可能限制收益。
Cache 层次
Cache 以 Cache Line 为单位传输数据。 程序只需要一个字节,也可能搬运整条 Line。
Cache miss 类型常分为:
- Compulsory:首次访问;
- Capacity:工作集超过容量;
- Conflict:映射冲突;
- Coherence:共享写导致失效。
分类用于提出方向,不同硬件计数器未必能直接精确区分全部类型。
预取
硬件预取器擅长连续和固定步长访问。 依赖指针链和随机访问难以提前发现。
软件预取只有在距离合适、访问确定且硬件预取不足时才可能有益。 过早会挤出有用数据,过晚则没有隐藏延迟。
数据布局
struct ParticleAoS {
float x, y, z, mass;
};
struct ParticleSoA {
std::vector<float> x;
std::vector<float> y;
std::vector<float> z;
std::vector<float> mass;
};只处理坐标的循环通常更喜欢 SoA。 频繁同时读取完整对象的逻辑可能更适合 AoS。
热冷字段拆分可以减少工作集。 AoSoA 可以在对象组织和向量宽度之间折中。
TLB 与页表
虚拟地址需要通过页表转换为物理地址。 TLB 缓存近期转换。
大工作集、随机访问和高线程数可能增加 TLB 压力。 大页减少页表项,却增加内存粒度、碎片和部署约束。
只有计数器与真实工作负载证明瓶颈后,才应引入大页。
Cache 一致性
多核系统需要让共享内存具有可理解行为。 核心修改 Cache Line 时,其他核心的副本可能被失效或更新。
频繁共享写会产生一致性流量。 即使线程修改不同变量,只要位于同一 Cache Line,也可能伪共享。
线程局部累加再归约,通常比每次更新全局原子更容易扩展。
NUMA 拓扑
多插槽系统中,本地内存通常比远端内存延迟低、带宽高。
Socket 0 CPUs <-> Local Memory 0
| interconnect
Socket 1 CPUs <-> Local Memory 1页面可能根据首次触碰位置分配。 单线程初始化后并行计算,会让其他节点大量远端访问。
NUMA 策略包括并行初始化、线程绑定、数据分片和只读复制。 应同时测量吞吐、远端访问和负载均衡。
GPU 执行资源
GPU 由多个执行簇组成,每个簇调度大量 Warp 或 Wavefront。
线程使用:
- 寄存器;
- 共享或本地数据存储;
- 多级 Cache;
- 全局设备内存;
- 常量和纹理路径;
- Host/Device 互连。
Block 大小会影响可驻留 Warp、寄存器和共享内存。 Occupancy 高只说明驻留条件,不证明指令和访存有效。
GPU 访存
相邻线程访问相邻地址时,更容易形成合并事务。 随机 Scatter/Gather 会增加内存事务和缓存压力。
共享内存用于 Block 内协作和数据复用。 访问模式不合适时可能产生 Bank Conflict。
Host 与 Device 传输必须进入端到端模型。 小 Kernel 即使执行极快,也可能被传输和启动完全覆盖。
异构计算
异构系统把控制、不规则任务和高吞吐 Kernel 分给不同处理器。
CPU
-> prepare and partition
-> launch accelerator work
-> overlap independent CPU work
-> synchronize required results
-> commit output分工边界应减少往返和细粒度同步。 在 CPU 与 GPU 之间反复搬运中间结果,常比计算本身更贵。
NPU 与专用加速器
专用加速器针对有限算子、精度和数据流优化能效。
评估时检查:
- 支持的数据类型;
- 动态形状;
- 算子覆盖;
- 编译时间;
- 内存容量;
- Host 回退;
- 数据传输;
- 数值容差;
- 工具和调试能力。
峰值 TOPS 不等于实际模型吞吐。 算子回退和布局转换可能显著降低端到端收益。
I/O 与设备
CPU 通过 MMIO、DMA、中断和驱动与设备协作。
DMA 允许设备直接搬运内存数据,减少 CPU 逐字节复制。 但仍需要缓冲区固定、同步和完成通知。
中断适合稀疏事件,高速设备可能使用轮询或中断合并降低开销。
性能计数器
硬件计数器可观察周期、指令、Cache、分支和内存事件。
解释计数器时注意:
- 事件定义依处理器而异;
- 计数器数量有限,可能时间复用;
- 推测执行会影响部分事件;
- 虚拟化可能限制可见性;
- 绝对值要与工作量归一化;
- 单个事件不能独立证明瓶颈。
计数器与调用栈、时间线和基准结合使用。
功耗与热限制
处理器性能受功耗、温度和频率策略影响。
长时间负载可能从短时 Boost 降到稳定频率。 只测几秒钟会高估持续任务性能。
向量宽度、核心数量和 GPU 功耗限制都会改变频率。 实验应记录预热、温度和电源计划。
体系结构实验
建议完成:
- 顺序与随机内存访问;
- 工作集跨越 Cache 层次;
- AoS 与 SoA;
- 分支可预测与随机分支;
- 单累加器与多累加器;
- 单线程与多线程扩展;
- 伪共享与填充;
- NUMA 本地和远端访问;
- GPU 合并与非合并访存;
- 传输加 Kernel 端到端测量。
每个实验保存硬件、编译参数、输入和原始样本。
实验结论还应回答:
- 工作量是否一致;
- 正确性是否保持;
- 冷热状态是否明确;
- 数据移动是否计入;
- 资源数量是否记录;
- 差异是否超过噪声;
- 瓶颈是否发生迁移;
- 结论适用于哪些规模;
- 不同硬件是否复现;
- 是否建立回归基准。
推荐路线
CUDA、SIMD 和 MPI 是如何使用这些硬件的编程模型,分别进入并行计算专题。
怎样验证理解
每个概念都应配合最小实验:改变数组访问步长观察缓存行为,比较 AoS 与 SoA 的向量化结果,固定线程亲和性检查 NUMA 放置,或把 GPU 传输时间与 Kernel 时间分开测量。实验要记录处理器、编译器、优化选项、输入规模和重复方法。
学完本路线后,应能从程序的访问模式推断潜在瓶颈,解释 CPU 与 GPU 的适用边界,并选择正确的计数器或 Profile 工具验证假设。具体型号参数会过时,但“工作如何分解、数据在哪里、等待由什么造成”这三个问题长期有效。