系统与高性能知识体系
这个大区回答三个连续问题:操作系统怎样管理资源,计算机怎样执行程序,以及怎样用测量证据改进性能。
应用看到的是线程、文件、地址和函数调用,硬件执行的却是指令、缓存访问、中断和设备传输。系统学习的目的,就是建立这两层之间的因果链。当程序变慢、崩溃或扩展性下降时,能够从现象追到调度、虚拟内存、Cache、NUMA、I/O 或通信,而不是只凭经验调整参数。
操作系统
├─ 进程、线程、调度
├─ 虚拟内存、文件和 I/O
└─ 启动、磁盘与存储
|
v
计算系统
├─ 体系结构:CPU/GPU 怎样执行
├─ 执行模型:串行、SIMD、线程、SIMT、进程
└─ 并行计算:OpenMP、CUDA、MPI 与并行模式
|
v
性能工程
└─ Benchmark、Profiling、Roofline 与回归验证1. 程序从源码到执行
源代码首先经过预处理、编译、汇编和链接,形成可执行文件与动态库。
Source
-> compiler front end
-> optimizer
-> object files
-> linker
-> executable / shared libraries
-> loader
-> process address space编译器把语言语义转换为目标指令。 优化会内联、消除无用代码、重排循环和进行向量化,但必须保持可观察行为。
链接器解析符号、合并 Section 并生成重定位信息。 装载器映射可执行文件和动态库,建立进程初始地址空间。
启动慢可能来自:
- 动态库数量和重定位;
- 磁盘与页面缺失;
- 全局构造;
- 配置和资源扫描;
- 字体、图形和 GPU 初始化;
- 网络和许可证连接;
- JIT 或 Shader 编译。
把启动看成完整时间线,才能定位真实阶段。
2. 进程与线程
进程提供资源和地址空间边界。 线程共享进程内存,但拥有独立栈、寄存器和调度状态。
Process
├─ virtual address space
├─ file descriptors / handles
├─ security context
├─ Thread A: stack + registers
├─ Thread B: stack + registers
└─ shared heap and mappings线程切换需要保存与恢复执行状态。 过多线程会增加调度、栈内存、Cache 抖动和同步成本。
进程隔离比线程强。 崩溃、依赖冲突和权限风险较高的模块适合进程外执行。
IPC 可以使用管道、Socket、共享内存和消息协议。 共享内存速度高,但同步与生命周期更加复杂。
3. 调度
操作系统调度器在可运行线程之间分配 CPU 时间。
线程状态大致包括:
- Running;
- Runnable;
- Sleeping;
- Waiting for I/O;
- Waiting for lock;
- Stopped。
CPU 利用率低时,要区分线程不可运行还是没有足够工作。 Runnable 很多而 Running 受限,说明 CPU 或配额饱和。
优先级不能替代容量设计。 错误使用高优先级可能饿死其他工作,并增加系统尾延迟。
线程亲和性适合稳定、拓扑敏感的计算任务。 交互和混合负载中,过度绑核可能限制调度器优化。
4. 虚拟内存
每个进程看到连续虚拟地址,页表把它映射到物理页或其他后端。
Virtual Address
-> page table walk
-> physical page
-> DRAM / mapped file / deviceTLB 缓存地址转换。 工作集很大或访问随机时,TLB miss 会增加页表遍历。
缺页分为不同类型。 匿名页首次访问、文件映射尚未驻留和真正磁盘换入的成本差异很大。
RSS、虚拟地址空间和提交量不是同一指标。 分析内存问题要区分可达对象、映射、共享页、页缓存和交换。
5. Cache 与内存层次
处理器访问寄存器和 Cache 比访问主存快得多。
Registers
-> L1
-> L2
-> LLC
-> Local DRAM
-> Remote NUMA DRAM
-> Storage / Network连续访问有利于 Cache Line 利用和硬件预取。 链式随机访问更容易受延迟限制。
时间局部性描述数据很快再次使用。 空间局部性描述相邻地址很快被访问。
数据布局会影响向量化、工作集和带宽。 AoS 与 SoA 没有绝对优劣,要看算法每次需要哪些字段。
6. 文件与 I/O
文件 API 背后可能经过页缓存、文件系统、块层、驱动和设备。
read / write
-> page cache
-> filesystem
-> block layer
-> device queue
-> storage device一次读取很快可能因为数据已经在页缓存。 一次写入返回也不一定代表数据已经持久化到介质。
可靠保存通常需要临时文件、Flush、同步和原子替换。 具体保证取决于操作系统和文件系统协议。
顺序大块 I/O 与随机小 I/O 的性能特征不同。 Benchmark 必须模拟真实访问模式。
7. CPU 执行
现代 CPU 通过流水线、乱序执行、分支预测和多级 Cache 降低单线程延迟。
前端负责取指、预测和译码。 后端把微操作调度到整数、浮点、向量和访存执行单元。
依赖链会限制指令级并行。 分支预测错误会丢弃错误路径上的工作。
IPC 是每周期退休指令数,但它不是跨工作负载通用性能指标。 相同任务完成时间和正确性才是最终结果。
SIMD 让一条指令处理多个元素。 连续数据、简单控制流和明确别名有利于自动向量化。
8. GPU 与异构系统
GPU 用大量线程和高带宽处理规则数据并行。 它更关注吞吐,而不是单个线程的最低延迟。
GPU 性能受到:
- 并行规模;
- Warp 发散;
- 内存访问合并;
- 寄存器与共享内存;
- Host/Device 传输;
- Kernel 启动与同步;
- 多 Stream 并发;
- 功耗和热限制。
CPU 与 GPU 应按任务特征分工。 不规则控制、操作系统服务和小任务通常留在 CPU,规则大规模 Kernel 交给 GPU。
端到端时间必须包含数据准备、传输和结果提交。
9. 网络路径
网络请求跨越应用、Socket、协议栈、网卡、交换设备和远端服务。
application buffer
-> socket
-> TCP / UDP / QUIC
-> kernel queues
-> NIC
-> network
-> remote stack and application延迟由传播、排队、处理、重传和应用等待共同组成。 带宽高不代表小消息延迟低。
TCP 提供有序可靠字节流,但应用仍需定义消息边界、超时和取消。 UDP 保留数据报边界,却把可靠性和顺序选择交给应用或上层协议。
10. 并发正确性
共享可变状态需要同步。 数据竞争、死锁、活锁和饥饿是不同问题。
锁建立互斥与内存可见性,但临界区过大会降低扩展性。 原子操作只保护对应对象,不自动保护复合不变量。
优先减少共享,再选择同步方式。 不可变快照、消息传递和线程局部状态经常比复杂无锁结构更可靠。
11. 性能工程闭环
define goal
-> fix workload
-> establish baseline
-> collect evidence
-> form falsifiable hypothesis
-> make minimal change
-> verify correctness and performance
-> guard regression工具选择取决于问题层次。
- 火焰图观察 CPU 调用路径;
- 时间线观察线程、I/O 和设备关系;
- 计数器解释 Cache、分支和带宽;
- Trace 连接分布式请求;
- Benchmark 验证改动是否可复现。
只看单个利用率指标不能证明瓶颈。
12. 可靠性与恢复
系统能力还包括失败后的行为。
需要考虑:
- 进程崩溃;
- 磁盘写满;
- 网络分区;
- 依赖超时;
- 任务取消;
- 机器重启;
- Checkpoint 损坏;
- 版本回滚;
- 资源泄漏。
错误应停在接近根因的边界,并携带足够上下文。 静默重试可能放大负载并隐藏真实故障。
13. 学习与实验方法
每个机制都应通过小实验验证:
- 改变数组步长观察 Cache 拐点;
- 改变线程数绘制扩展曲线;
- 比较串行、SIMD 和多线程;
- 区分冷缓存与热缓存 I/O;
- 固定亲和性观察 NUMA;
- 分离 GPU 传输与 Kernel;
- 注入锁竞争和网络延迟;
- 保存环境与原始样本。
实验目标是建立可证伪理解,而不是得到一个漂亮数字。
每次结论都应能指出对应的测量命令、输入数据和适用硬件范围。
权威入口
原来的并行计算目录保留旧链接兼容,但不再维护第二套权威正文。
一条程序实际经历了什么
源代码
│ 编译、链接与装载
v
进程虚拟地址空间
│ 调度到某个逻辑处理器
v
指令流水线 <-> Cache / 主存 <-> NUMA 或设备内存
│ │
│ └─ DMA、磁盘、网络、GPU
v
系统调用、缺页、中断与上下文切换这张图说明性能问题通常跨越多层。一次看似普通的数组遍历可能受编译器向量化、缓存行、页映射和 NUMA 放置共同影响;一次文件读取可能经过页缓存而没有真正访问磁盘;GPU Kernel 很快,也可能被主机到设备的数据传输抵消。学习时应持续追问:数据在哪里、由谁移动、执行单元在等待什么、观测指标来自哪一层。
推荐学习方式
第一阶段先掌握进程、线程、虚拟内存、文件系统和 I/O,能解释程序获得资源以及隔离如何实现。第二阶段进入 CPU 流水线、缓存层次、SIMD、GPU SIMT 和内存一致性,理解相同算法为何在不同数据布局下表现悬殊。第三阶段学习线程、OpenMP、CUDA 与 MPI,把工作分解、同步、通信和负载均衡联系起来。最后进入性能工程,用实验设计和工具验证前面形成的假设。
每学习一个机制,都应做一个可以证伪的小实验。例如改变遍历顺序观察 Cache 行为,固定输入规模比较线程数,区分冷缓存与热缓存,或用采样 Profile 验证热点是否真的位于预期函数。实验需要记录硬件、编译参数、输入、预热、重复次数和统计方法,否则结果无法复现。
与其他大区的边界
- C++ 区讲
std::thread、原子和构建工具怎样使用;本区解释核心、Cache、SIMD、GPU 和 MPI 为什么这样工作。 - AI Infra 讲训练系统;本区提供 CUDA、集合通信和硬件的通用基础。
- 工业软件讲求解算法;本区提供数据局部性、并行模式和性能测量方法。
边界不是割裂。C++ 并发语义决定代码是否正确,硬件内存模型解释它为何需要同步;算法决定理论工作量,体系结构决定数据移动成本;部署系统给出资源限制,性能工程判断限制是否构成瓶颈。遇到跨区问题时,应从业务目标向下追踪,而不是为了使用某个工具而优化。
统一性能模型
总时间 = 有效计算 + 数据移动 + 同步等待 + 调度开销 + I/O先保证正确,再测量瓶颈,最后优化。CPU 利用率或 GPU Occupancy 等单个指标不能独立证明程序高效。
完整优化闭环是:定义用户可感知指标,固定代表性工作负载,建立基线,采集证据,提出一个可检验假设,只做最小改动,然后复测正确性、性能和资源成本。局部加速如果增加端到端延迟、破坏数值结果或让维护成本失控,就不属于成功优化。
学完本区后,读者应能画出程序从源代码到硬件执行的路径,选择合适的并行模型,判断测量是否可信,并用瓶颈证据解释优化有效或无效的原因。