Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

本页目录

CPU 并行:多线程、SIMD、Cache 一致性与 NUMA / CPU Parallelism with Threads, SIMD, Cache Coherence, and NUMA ​

📅 创建时间:2026-07-20 🏷️ 标签:#CPU #多线程 #SIMD #NUMA #OpenMP 📚 前置知识:[[02-processor-architecture]] 📚 相关知识:[[/02-systems-and-performance/02-computer-architecture-and-hardware/07-openmp-simd]] [[/01-cpp/06-concurrency/04-concurrency]]


1. CPU 并行的三个层次 ​

text
线程级并行:多个核心执行不同线程
数据级并行:一条 SIMD 指令处理多个元素
任务级并行:不同任务组成流水线或任务图
1
2
3

例如矩阵运算可以把行分给多个线程,每个线程内部再使用 AVX 指令一次计算多个浮点数。


2. 线程并行 ​

线程创建不是免费的 ​

创建和销毁线程涉及系统调用、栈空间和调度,因此工程中通常使用线程池。

text
任务提交 → 工作队列 → 固定数量工作线程 → 执行结果
1

线程数量并非越多越好:

  • 计算密集型任务通常接近物理核心数
  • I/O 密集型任务可以更多
  • 使用 SMT 时需要实际测量
  • 第三方数学库可能已经创建线程,避免嵌套过度并行

OpenMP 示例 ​

cpp
#pragma omp parallel for
for (int i = 0; i < n; ++i) {
    output[i] = compute(input[i]);
}
1
2
3
4

OpenMP 适合逐步并行化规则循环,但仍需检查数据竞争和任务粒度。


3. SIMD 向量化 ​

标量加法一次处理一个元素:

text
a0+b0 → c0
1

256 位 AVX 指令可以一次处理 8 个 FP32:

text
[a0..a7] + [b0..b7] → [c0..c7]
1

编译器自动向量化偏爱:

  • 连续内存访问
  • 简单、固定次数的循环
  • 迭代之间没有依赖
  • 容易证明指针不重叠
  • 分支较少

不利示例 ​

cpp
for (int i = 1; i < n; ++i) {
    a[i] = a[i - 1] * 0.5f; // 当前迭代依赖前一次结果
}
1
2
3

这是循环携带依赖,不能直接并行执行各次迭代。


4. 数据竞争与同步 ​

cpp
counter++; // 读取、加一、写回,并非不可分割操作
1

多个线程同时修改会导致结果丢失。常用保护方式:

  • Mutex:保护复杂临界区
  • Atomic:保护简单原子状态
  • Reduction:每个线程局部计算,最后合并
  • 消息传递:避免共享可变状态

并行归约通常优于每次循环都锁住全局变量。


5. 伪共享 ​

即使线程修改不同变量,只要变量位于同一个 Cache Line,也可能互相使缓存失效。

cpp
struct Counters {
    long a; // 线程 A 修改
    long b; // 线程 B 修改,但可能与 a 在同一 Cache Line
};
1
2
3
4

常见处理方法:

  • 对高频写入的线程局部变量进行 Cache Line 对齐
  • 每线程维护局部统计值
  • 降低共享写入频率

伪共享不会造成结果错误,但会造成严重性能下降。


6. NUMA:内存也有远近 ​

多路服务器中,每颗 CPU 插槽通常连接自己的本地内存。

text
CPU 0 ─ 本地内存 0
  │
互联总线
  │
CPU 1 ─ 本地内存 1
1
2
3
4
5

CPU 0 访问内存 1 的延迟更高、带宽可能更低,这就是非统一内存访问 NUMA。

NUMA 优化原则 ​

  • First Touch:由实际使用数据的线程首次初始化数据
  • 线程绑定:避免线程在不同 NUMA 节点之间迁移
  • 数据分区:让线程主要访问本地内存
  • 测量远程访问比例,而不是凭感觉绑定

7. CPU 并行适用场景 ​

CPU 更适合:

  • 分支复杂、任务差异大的计算
  • 数据规模较小、要求低延迟的计算
  • 图遍历、搜索、编译、数据库执行
  • 操作系统和 I/O 协调
  • GPU kernel 前后的数据准备与控制

GPU 更适合并不意味着 CPU 不重要。多数异构应用都依赖 CPU 组织整个执行流程。


8. 线程池生命周期 ​

线程池在应用生命周期内复用 Worker。

定义启动、队列、并发、取消、Drain、Shutdown 和异常。

关闭时停止新任务,处理现有任务,再 Join Worker。

9. 队列 ​

中央队列简单,但可能成为锁与 Cache 热点。

每线程队列加 Work Stealing 适合不规则任务。

有界队列提供背压,防止内存无限增长。

10. 静态调度 ​

规则、成本相近的循环适合静态分块。

优势是调度少、局部性好、行为可预测。

输入不均时会产生长尾。

11. 动态调度 ​

动态队列适合任务成本不规则。

Chunk 太小增加调度,太大降低均衡。

Guided 调度从大块逐步缩小。

12. Reduction ​

每线程局部累加,最后树形合并。

text
local0 local1 local2 local3
   \ /          \ /
   sum01        sum23
         \    /
          total
1
2
3
4
5

浮点顺序变化需要容差。

13. 锁粒度 ​

粗锁容易正确但并行度低。 细锁增加顺序、死锁和维护复杂度。

优先删除共享、分片、缩小临界区和批处理。

14. 条件变量 ​

条件变量等待状态谓词。

cpp
cv.wait(lock, [&] { return stop || !queue.empty(); });
1

谓词处理虚假唤醒。

15. 原子顺序 ​

seq_cst 最易推理。

Acquire/Release 适合发布与获取。

Relaxed 不发布周围普通数据。

只有证明热点且协议正确后才弱化。

16. False Sharing 优化 ​

  • 每线程数据;
  • 分片;
  • 对齐;
  • 批量合并;
  • 减少写频率。

使用计数器和扩展曲线验证。

17. NUMA 策略 ​

并行初始化实现 First Touch。

每节点队列和内存池减少远端访问。

最终在粗粒度合并。

18. SIMD 与线程 ​

线程处理外层块,SIMD 处理块内连续元素。

布局、对齐、别名和尾部决定向量化。

19. 异常 ​

Worker 异常不能悄悄终止线程。

任务系统捕获并写入 Future 或任务结果。

20. 取消 ​

使用 Stop Token 或共享取消状态。

任务在安全点检查并清理资源。

21. 调试 ​

  • Thread Sanitizer;
  • 死锁检测;
  • 日志关联;
  • 压力循环;
  • 故障注入;
  • 单线程对照;
  • 确定性调度测试。

22. Profile ​

观察:

  • 每线程 CPU 时间;
  • Running/Ready/Wait;
  • 锁等待;
  • 队列;
  • 任务数;
  • False Sharing;
  • NUMA;
  • 串行阶段。

WPA 和 VTune Threading 可用于分析。

23. 扩展实验 ​

测量线程数 1、2、4、8…

记录时间、加速比、效率、锁、带宽、远端内存与正确性。

24. 完成标准 ​

应能:

  • 设计线程池;
  • 选择调度;
  • 正确归约;
  • 管理锁与条件变量;
  • 使用原子顺序;
  • 识别 False Sharing;
  • 处理 NUMA;
  • 组合 SIMD;
  • 取消与关闭;
  • Profile 扩展性。

25. 实验记录 ​

CPU 并行实验还应保存:

  • CPU 与拓扑;
  • 编译器;
  • OpenMP/运行库;
  • 线程数;
  • 调度和 Chunk;
  • 亲和性;
  • NUMA 放置;
  • SIMD 报告;
  • 输入;
  • 预热;
  • 原始样本;
  • 锁等待;
  • 内存带宽;
  • 正确性。 这些信息使扩展曲线能够在其他机器复核。 缺失环境时不应外推线程结论。

章节检查清单 ​

  • 循环迭代是否相互独立?
  • 任务粒度是否显著大于调度成本?
  • 是否存在共享写入和伪共享?
  • 编译器是否成功向量化?
  • 线程和数据是否跨越 NUMA 节点?
  • 数学库是否已经在内部并行?

下一篇:[[04-memory-hierarchy]]

最后更新于:

Pager
下一篇系统与高性能知识体系

持续记录,持续成长

Copyright © Tidenflow