从 C++ 源码到 CPU 执行
先回答:CPU 到底做什么
CPU 不理解 C++ 的类、循环或变量名。它读取由指令集架构(ISA)定义的机器指令,操作寄存器和内存,并按照控制流决定下一条指令地址。
main.cpp
| compiler: parse, optimize, generate code
v
main.obj / main.o ----+
| linker: resolve symbols and relocate addresses
library.obj / .o -----+
v
executable
| OS loader: map code/data, create process and first thread
v
CPU instruction streamISA 是软件和处理器之间的契约,例如 x86-64、AArch64;微架构是某款 CPU 实现该 ISA 的内部方式,例如流水线宽度、Cache 大小和执行端口。相同机器指令可以在不同微架构上得到相同结果,却有不同速度。
一个循环怎样执行
#include <cstddef>
int sum(const int* values, std::size_t count) {
int result = 0;
for (std::size_t i = 0; i < count; ++i) {
result += values[i];
}
return result;
}概念化的指令序列可能是:
loop:
load r1, [values + i*4] ; 从内存层次取得元素
add result, result, r1 ; 更新累加器
add i, i, 1 ; 更新索引
compare i, count
branch_less loop ; 改变下一条指令地址编译器可以完全改写这份结构:展开循环、消除索引、使用 SIMD,甚至在结果无用时删除整个计算。概念图用于理解数据依赖,不能冒充某个编译器的固定汇编。
取指到退休
program counter
|
v
Fetch -> Decode -> Rename/Issue -> Execute -> Memory -> Retire
^ |
+-------------- next predicted address ----------------+- Fetch:根据下一条地址取得指令字节;
- Decode:识别操作和操作数;
- Execute:算术逻辑单元、地址单元等执行工作;
- Memory:load/store 访问 Cache/内存层次;
- Retire:按程序顺序提交可观察结果。
现代高性能 CPU 常加入译码缓存、寄存器重命名、乱序调度和多个执行端口,后文再拆解。
程序、进程、线程与核心
executable file
|
v
process: address space + resources
|
+--> thread A --OS schedules--> logical CPU 3
+--> thread B --OS schedules--> logical CPU 7- 程序是文件;进程是运行中的资源容器;线程是 OS 调度的执行流;
- 一个线程在某一时刻通常运行于一个逻辑 CPU,但可能在时间片之间迁移;
- 一个物理核心可能提供多个硬件线程(SMT);
- 单线程仍可利用 CPU 内部的指令级并行和 SIMD;
- 普通编译器通常不会凭空为一般循环创建多个软件线程,因为它无法普遍证明依赖、异常和副作用仍正确。
怎样观察真实结果
g++ -std=c++17 -O2 -Wall -Wextra -S main.cpp
objdump -d -C appMSVC 可生成汇编列表或在调试器 Disassembly 窗口查看。必须记录编译器版本、优化级别、目标架构和输入,否则汇编比较没有上下文。
常见错误
- 认为 CPU 直接执行 C++;
- 把 ISA 和某一代 CPU 的内部结构混为一谈;
- 认为单线程一定只会同时执行一条底层操作;
- 认为打开
-O3就会自动安全使用所有核心; - 从少量汇编推导所有编译器和平台。
深入原理与工程实践
前面的内容负责建立统一心智模型;下面把同一主题继续拆到执行过程、代码、性能代价与工程判断。
<!-- migrated-deep-dive:start -->
完整迁入:原体系结构:CPU 设计、内存层次与 NUMA
计算机架构基础——为什么 GPU 比 CPU 更快 / Computer Architecture Fundamentals: Why GPUs Outperform CPUs
📅 创建时间:2026-06-02 🏷️ 标签:#Hardware #CPU #Cache #内存层次 #NUMA #SIMD 📚 前置知识:[[00-hardware-overview]](硬件全景 + 工具链) 📚 相关知识:[[03-gpu-architecture]](GPU 与 CPU 的对比) [[02-parallel-computing-theory]](并行理论)
先抓住直觉
CPU 像一位反应很快、能处理复杂订单的主厨;GPU 像一间有大量工位、适合重复流水作业的厨房。真正决定速度的不只有“厨师算得多快”,还包括食材是否就在手边。
- 必须理解:CPU 与 GPU 的设计取舍;延迟与带宽;数据局部性。
- 用到再查:具体 Cache 容量、纳秒数、NUMA API 和 SIMD 指令宽度。
- 读完能回答:为什么连续遍历数组通常比随机访问快?为什么“GPU 核心更多”不等于任何程序都更快?
场景:为什么训练 AI 模型用的是 GPU 而不是 CPU?
┌─────────────────────────────────────────────────────────────┐
│ │
│ 凌晨 2 点,你盯着训练日志发呆。 │
│ │
│ 模型:A100 单卡,175B 参数 │
│ 当前速度:每秒处理 1500 个 Token │
│ 预计总训练时间:30 天 │
│ │
│ 你的同事在用同样的模型、同样的数据,却只需要 3 天。 │
│ 区别:他用 8 卡并行训练 │
│ │
│ 你开始思考一个问题: │
│ 为什么 AI 训练用 GPU 而不是 CPU? │
│ 多卡并行为什么能加速?理论极限在哪里? │
│ │
└─────────────────────────────────────────────────────────────┘这一章,我们从最底层出发,理解计算机的硬件结构,为后续理解 GPU 打下基础。
第1节:CPU 的设计哲学——单核要足够快
CPU 是什么?
CPU(Central Processing Unit,中央处理器)是计算机的大脑。它的设计哲学是:单个任务要执行得足够快。
打开任务管理器,你会看到 CPU 的这些指标:
- 频率:3.5 GHz(每秒 35 亿个时钟周期)
- 核心数:8 核(现代桌面 CPU)
- 线程数:16 线程(超线程技术)
CPU 的架构解剖
┌─────────────────────────────────────────────────────────────┐
│ CPU 核心内部结构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ │
│ │ Fetch │ ← 取指:从内存取下一条指令 │
│ └────┬────┘ │
│ ↓ │
│ ┌─────────┐ │
│ │ Decode │ ← 译码:解析这条指令要做什么 │
│ └────┬────┘ │
│ ↓ │
│ ┌─────────┐ │
│ │ ALU │ ← 执行:算术逻辑运算(加减乘除、与或非) │
│ └────┬────┘ │
│ ↓ │
│ ┌─────────┐ │
│ │ Write │ ← 写回:把结果写回寄存器 │
│ └─────────┘ │
│ │
│ ┌─────────┐ │
│ │Registers│ ← 寄存器:最快的存储,速度≈ CPU 同频 │
│ │ (64个) │ 每个寄存器 64 位(8 字节) │
│ └─────────┘ │
│ │
│ ┌─────────┐ │
│ │ L1 Cache│ ← 一级缓存:~32KB,极快,约 1ns │
│ └────┬────┘ │
│ ↓ │
│ ┌─────────┐ │
│ │ L2 Cache│ ← 二级缓存:~256KB,快,约 3ns │
│ └────┬────┘ │
│ ↓ │
│ ┌─────────┐ │
│ │ L3 Cache│ ← 三级缓存:~32MB,较慢,所有核心共享,约 10ns │
│ └────┬────┘ │
│ ↓ │
│ ┌─────────┐ │
│ │ Memory │ ← 主内存(DDR):~64GB,慢,约 100ns │
│ └─────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘C++ 程序员的视角
当你写一个 C++ 循环时:
int sum = 0;
for (int i = 0; i < 1000000; i++) {
sum += data[i]; // 每次访问 data[i],数据可能不在寄存器里
}CPU 在背后做的事情:
第1次访问 data[0]:
→ 寄存器里没有 → 查 L1 Cache,没有
→ 查 L2 Cache,没有 → 查 L3 Cache,没有
→ 终于从主内存读出来了(100ns!)
第2次访问 data[1]:
→ 如果 data[1] 紧挨着 data[0],它在 L1 Cache 里(1ns!)
这就是 Cache 的意义:利用**数据局部性**(Spatial Locality),
把刚用过的数据和它附近的数据一起预取到 Cache 里。第2节:内存层次——速度与容量的 trade-off
内存金字塔
┌─────────────────────────────────────────────────────────────┐
│ 内存层次金字塔 │
├─────────────────────────────────────────────────────────────┤
│ │
│ CPU 寄存器 │
│ [ 64个 × 8B ] │
│ ≈ 0.5 KB │
│ 访问延迟: ~0.1ns │
│ 带宽: ∞(CPU 同频) │
│ ↑ │
│ L1 Cache │
│ ≈ 32-64 KB │
│ 访问延迟: ~1ns │
│ 带宽: ~1 TB/s │
│ ↑ │
│ L2 Cache │
│ ≈ 256 KB │
│ 访问延迟: ~3ns │
│ 带宽: ~500 GB/s │
│ ↑ │
│ L3 Cache │
│ ≈ 16-64 MB │
│ 访问延迟: ~10ns │
│ 带宽: ~200 GB/s │
│ ↑ │
│ 主内存 DDR4 │
│ ≈ 32-256 GB │
│ 访问延迟: ~100ns │
│ 带宽: ~50 GB/s │
│ ↑ │
│ NVMe SSD │
│ ≈ 1-8 TB │
│ 访问延迟: ~100,000ns (0.1ms) │
│ 带宽: ~5 GB/s │
│ ↑ │
│ 机械硬盘 │
│ ≈ 10-20 TB │
│ 访问延迟: ~10,000,000ns (10ms) │
│ 带宽: ~200 MB/s │
│ │
└─────────────────────────────────────────────────────────────┘关键指标:延迟 vs 带宽
延迟(Latency):做一次操作需要多久
- L1 Cache:1ns = 你眨眼速度的 3 亿倍
- 主内存:100ns = L1 的 100 倍
- NVMe SSD:100,000ns = L1 的 10 万倍
带宽(Bandwidth):单位时间内能传输多少数据
- L1 Cache:~1 TB/s
- 主内存:~50 GB/s(DDR4-3200)
- A100 GPU HBM2e:~2 TB/s(比 CPU 内存快 40 倍!)
重要结论:
CPU 设计针对**低延迟**优化(分支预测、Cache 预取)。
GPU 设计针对**高带宽**优化(大量并行数据访问)。Cache Line:内存访问的基本单位
Cache 不是按字节传输的,而是按 Cache Line(缓存行)为单位传输。
Cache Line 大小 = 64 字节(现代 CPU 的标准)
假设你访问一个 int(4 字节):
data[0] = 0x1000 地址
CPU 实际做的是:
→ 把地址 0x1000 ~ 0x103F(64字节)全部读入 L1 Cache
→ 然后只取你要的 4 字节
这意味着:
访问 data[0] 后,data[1] ~ data[15] 也在 Cache 里了(如果它们连续)
这就是**顺序访问比随机访问快很多**的原因。C++ 实战:如何让代码利用 Cache 局部性
// ❌ 慢:Cache 不友好(跳跃式访问)
// stride = 4096 意味着每次跳过一个 Cache Line
// 每次访问都会触发从主内存加载(100ns)
for (int i = 0; i < 1000000; i += 4096) {
sum += data[i]; // Cache 永远 miss
}
// ✅ 快:Cache 友好(顺序访问)
// data[0], data[1], data[2]... 都在同一个 Cache Line 里
// 只有每 16 个数才需要加载新的 Cache Line
for (int i = 0; i < 1000000; i++) {
sum += data[i]; // 大部分命中 L1 Cache
}第3节:为什么 CPU 不适合 AI 训练?
CPU 的串行瓶颈
AI 训练的核心运算是矩阵乘法:
矩阵乘法:Y = W × X
W: 4096 × 4096(权重矩阵)
X: 4096 × 1(输入向量)
Y: 4096 × 1(输出向量)
CPU 怎么做:
for (int i = 0; i < 4096; i++) { // 外层循环:4096 次
for (int j = 0; j < 4096; j++) { // 内层循环:4096 × 4096 = 1600万次
Y[i] += W[i][j] * X[j];
}
}
一个 CPU 核心每次只能做一次乘加运算。
4096 × 4096 = 1600 万次乘加运算,
在 3.5 GHz 的 CPU 上,假设每个运算 1 个周期,需要 4.5ms。CPU 的多核并行
现代 CPU 有多核:
┌─────────────────────────────────────────────────────────────┐
│ 16 核 CPU 架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐
│ │Core│ │Core│ │Core│ │Core│ │Core│ │Core│ │Core│ │Core│
│ │ 0 │ │ 1 │ │ 2 │ │ 3 │ │ 4 │ │ 5 │ │ 6 │ │ 7 │
│ └────┘ └────┘ └────┘ └────┘ └────┘ └────┘ └────┘ └────┘
│ │ │ │ │
│ L2 Cache L2 Cache L2 Cache L2 Cache
│ └──────────┴──────────┴──────────┘
│ ↑
│ 共享 L3 Cache
│ │
│ ┌────┴────┐
│ │ 内存控制器 │
│ └────┬────┘
│ │
│ DDR4 内存
│ │
└─────────────────────────────────────────────────────────────┘
所有核心共享同一条通往内存的通道(总线带宽有限)。
即使 16 核同时跑,内存带宽也是瓶颈。CPU 的 SIMD:有限的向量化
CPU 有 SIMD(Single Instruction Multiple Data,单指令多数据)单元:
// 普通 C++(标量):一次处理一对数
float a = x[i];
float b = y[i];
float c = a + b;
// SIMD(向量化):一次处理 8 对数(AVX-512 = 512位 = 16个float)
// 需要编译器支持,或者手动用 intrinsics
#include <immintrin.h>
__m512 va = _mm512_loadu_ps(&x[i]); // 一次加载 16 个 float
__m512 vb = _mm512_loadu_ps(&y[i]);
__m512 vc = _mm512_add_ps(va, vb); // 一次加 16 对
_mm512_storeu_ps(&z[i], vc); // 一次存储 16 个结果CPU 的 SIMD 限制:
- 最多同时处理 16 个 float(AVX-512)
- 而且 SIMD 单元数量有限,不是所有运算都能用上
- 16 个并行 vs GPU 的 1024 个并行,差距是 64 倍
第4节:NUMA——多路服务器的特殊性
为什么你的服务器有多个插槽?
高性能服务器通常有多个 CPU 插槽:
┌─────────────────────────────────────────────────────────────┐
│ 双路服务器架构(NUMA) │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────┐ ┌────────────────┐ │
│ │ CPU Socket 0 │ │ CPU Socket 1 │ │
│ │ (8 核心) │ │ (8 核心) │ │
│ └───────┬────────┘ └───────┬────────┘ │
│ │ │ │
│ L3 Cache L3 Cache │
│ │ │ │
│ ┌───────┴────────┐ ┌───────┴────────┐ │
│ │ DDR4 通道1 │ │ DDR4 通道2 │ │
│ │ (本地内存) │ │ (本地内存) │ │
│ │ 128 GB │ │ 128 GB │ │
│ └────────────────┘ └────────────────┘ │
│ │ │ │
│ └──────────┬────────────────┘ │
│ │ │
│ QPI/UPI 总线(跨插槽访问延迟高 2-3 倍) │
│ │
└─────────────────────────────────────────────────────────────┘NUMA 的问题
CPU Socket 0 访问本地内存:延迟 ~100ns
CPU Socket 0 访问 Socket 1 的内存:延迟 ~200-300ns(通过 QPI 总线)
如果你的程序没有考虑 NUMA:
→ 数据可能在 Socket 1 的内存里
→ 但 Socket 0 的核心在计算
→ 每次内存访问都要跨总线(慢 2-3 倍)NUMA 感知编程(C++ 示例)
#include <numa.h>
#include <numaif.h>
int main() {
numa_set_strict(1); // 强制 NUMA 策略
// 在 CPU Socket 0 上分配内存
void* local_mem = numa_alloc_onnode(1024 * 1024 * 1024, 0);
// 在 CPU Socket 0 上绑定线程
struct bitmask* mask = numa_allocate_nodemask();
numa_bitmask_setbit(mask, 0); // 只用 Socket 0
numa_bind(mask);
// 之后的 malloc 都会在 Socket 0 上分配
double* data = (double*)malloc(1024 * 1024 * 1024 * sizeof(double));
// 或者用 OpenMP 指定 NUMA 策略
#pragma omp parallel
{
int node = numa_node_of_cpu(omp_get_thread_num());
// 每个线程绑定到它所在 NUMA 节点的 CPU
}
}第5节:从 CPU 到 AI 芯片——为什么 GPU 天生适合矩阵运算
矩阵乘法的本质
回到 4096 × 4096 的矩阵乘法:
Y[i][j] = Σ W[i][k] × X[k][j](k 从 0 到 4095)
展开看:
Y[i][j] = W[i][0]×X[0][j] + W[i][1]×X[1][j] + ... + W[i][4095]×X[4095][j]
每个 Y[i][j] 的计算完全独立!
4096 × 4096 = 1600 万个输出元素,每个都可以**并行计算**。CPU vs GPU 的核心差异
┌─────────────────────────────────────────────────────────────┐
│ CPU vs GPU:设计哲学差异 │
├─────────────────────────────────────────────────────────────┤
│ │
│ CPU:让一个任务执行得极快 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ [ALU][ALU][ALU][ALU] ← 4-8 个算术单元 │ │
│ │ ↑ │ │
│ │ [大缓存] [复杂分支预测] [乱序执行] [超线程] │ │
│ │ ↑ │ │
│ │ [通用控制逻辑](能处理任何类型的计算) │ │
│ │ │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ GPU:同时执行大量简单任务 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ [ALU][ALU][ALU][ALU]...[ALU][ALU][ALU][ALU] │ │
│ │ [ALU][ALU][ALU][ALU]...[ALU][ALU][ALU][ALU] │ │
│ │ [ALU][ALU][ALU][ALU]...[ALU][ALU][ALU][ALU] │ │
│ │ ↑ │ │
│ │ (几千个算术单元!) │ │
│ │ │ │ │
│ │ [小缓存] [简单控制逻辑] [专为矩阵运算优化] │ │
│ │ │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ 总结: │
│ CPU:单核极快,但核心少(8-64核) │
│ GPU:单核简单,但核心极多(上千核) │
│ │
│ 矩阵乘法:1600 万个独立计算,正好用 GPU 的海量核心! │
│ │
└─────────────────────────────────────────────────────────────┘数字对比
┌─────────────────────────────────────────────────────────────┐
│ A100 vs 高端 CPU 对比 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 指标 │ A100 GPU │ AMD EPYC 7763 (CPU)│
│ ────────────────┼──────────────────┼─────────────────────┤
│ 核心数 │ 6912 个 CUDA 核 │ 64 核 │
│ 峰值算力(FP32) │ 19.5 TFLOPS │ ~1 TFLOPS │
│ 内存带宽 │ 2.0 TB/s (HBM2e)│ ~200 GB/s │
│ 内存容量 │ 40/80 GB │ 256 GB │
│ 功耗 │ 400W │ 280W │
│ │
│ 矩阵乘法性能差距:约 20-50 倍 │
│ │
└─────────────────────────────────────────────────────────────┘这就是为什么 AI 训练用 GPU 而不是 CPU:当你要做大量独立的简单计算时,GPU 的海量简单核心远胜 CPU 的少量复杂核心。
升华:这一章解决了什么问题?
┌─────────────────────────────────────────────────────────────┐
│ 本章核心知识点串联 │
├─────────────────────────────────────────────────────────────┤
│ │
│ CPU 的设计哲学:单核极快 → 但核心少 → 矩阵乘法慢 │
│ │
│ 内存层次:L1 Cache(1ns) → L2(3ns) → L3(10ns) → 内存(100ns)│
│ → 理解 Cache Line 和局部性是性能优化的基础 │
│ │
│ NUMA:多路服务器的内存访问延迟差异 → NUMA 感知编程重要性 │
│ │
│ GPU 天生适合 AI:海量简单核心 + 高带宽内存 │
│ → 这就是为什么大模型训练必须用 GPU │
│ │
│ 下一章预告:GPU 的理论加速比是多少? │
│ → 阿姆达尔定律告诉你答案 │
│ │
└─────────────────────────────────────────────────────────────┘"AI 可查 vs 必须理解"清单
AI 可查:
✅ L1/L2/L3 Cache 的大小和延迟的具体数值——各型号不同,文档里查
✅ AVX-512 intrinsics 的具体函数名——_mm512_add_ps 等,文档里有
✅ NUMA API 的具体函数——numa_alloc_onnode 等,头文件里查
✅ CPU 各型号的具体参数——核心数/频率/功耗,官网有
必须理解:
🔴 Cache Line = 64 字节,顺序访问比随机快
🔴 延迟:L1(1ns) < L2(3ns) < L3(10ns) < Memory(100ns) < SSD(100us)
🔴 CPU 适合低延迟串行任务,GPU 适合高带宽并行任务
🔴 矩阵乘法的 1600 万个独立计算正好适合 GPU 的海量核心
🔴 NUMA:跨插槽访问内存延迟高 2-3 倍学习状态:🟡 开始学习 <!-- migrated-deep-dive:end -->
面试速答
C++ 程序怎样在 CPU 上运行?
编译器把源码转换为目标代码,链接器解析符号和重定位生成可执行文件;OS 装载文件、建立进程和线程,CPU 再按照 ISA 执行机器指令。流水线和乱序执行属于常见微架构实现。
单线程等于单核吗?
不等于。线程是软件执行流,核心是硬件资源;线程可被 OS 迁移,单个线程内部也可能利用 ILP 与 SIMD,但要同时长期占用多个核心通常需要多个可运行线程。
自测与答案
- ISA 与微架构有何区别? **答案:**ISA 定义软件可见的指令与状态契约;微架构描述处理器怎样实现它。
- 编译器为什么不能普遍把循环自动变成多线程? **答案:**依赖、副作用、异常、顺序和线程安全无法对任意程序自动证明,线程创建也有成本。
- 进程与线程谁拥有地址空间? **答案:**进程提供地址空间与资源容器,同一进程的线程通常共享该地址空间。