NUMA 与多路 CPU 访存
NUMA 与多路 CPU 访存
时间:2026/05/08
关键词:NUMA、socket、本地内存、远端内存、first touch、线程亲和性、内存带宽、cache coherence
核心目标:理解多路 CPU 机器上“内存离哪个核更近”,以及怎样避免并行程序被远端访存拖慢。
1. NUMA 是什么
NUMA 是 Non-Uniform Memory Access。
意思是:
CPU 访问不同位置的内存,成本不一定一样。
在单路桌面机器上,可以先粗略认为 CPU 访问内存的距离差异不明显。
但在多路服务器上,每个 CPU socket 往往连接着自己附近的一组内存通道。
如果线程运行在 socket 0,却频繁访问 socket 1 附近的内存,就会发生远端访存。
远端访存通常有更高延迟,也会消耗 socket 之间互连带宽。
2. UMA 和 NUMA 的直觉差异
UMA:
1 | 所有核心访问内存成本近似一样 |
NUMA:
1 | 核心访问本地内存更快 |
一个很粗略的模型:
1 | socket 0 ---- local memory 0 |
线程、数据、内存页的位置都开始影响性能。
3. 为什么高性能 C++ 要关心 NUMA
很多并行程序在单 socket 内扩展很好,但跨 socket 后突然变差:
1 | 1 线程: 1x |
可能原因不是线程库不好,而是:
- 远端内存访问增加
- socket 间互连带宽被打满
- 跨 socket 共享数据导致 cache line 来回迁移
- 锁或原子变量集中在一个 NUMA node
- 内存初始化在一个线程完成,页面都落在一个 node
NUMA 问题常常不是小数据问题,而是大数据、高吞吐、多线程问题。
4. first touch 原则
很多操作系统采用 first touch 策略:
内存页第一次被哪个 CPU 附近的线程写入,就倾向于分配到那个 NUMA node。
坏模式:
1 | std::vector<double> a(n); |
如果初始化线程在 socket 0 上运行,大量页面可能被放到 socket 0。
后续 socket 1 上的线程访问自己负责的数据时,也可能是在远端读写。
更好的模式:
1 | std::vector<double> a(n); |
核心思想:
谁负责处理某段数据,最好也由谁先初始化那段数据。
5. NUMA 下最重要的数据划分方式
最常见的好模式是静态分区:
1 | socket 0/thread group 0 处理 a[0, mid) |
每个分区尽量满足:
- 本线程组初始化
- 本线程组主要读写
- 本线程组局部归约
- 最后只合并小结果
坏模式通常是:
1 | 所有线程随机访问整个大数组 |
这些模式会把本地访问变成远端访问,把局部缓存变成跨 socket 通信。
6. 跨 socket 共享 cache line 的成本
已有的 false sharing 在 NUMA 上会更痛。
例如每个线程写自己的计数器:
1 | std::vector<std::uint64_t> counters(num_threads); |
如果多个计数器落在同一 cache line,cache line 会在核心之间迁移。
如果这些核心跨 socket,迁移成本更高。
更好的做法:
1 | struct alignas(64) Counter { |
这不能解决所有 NUMA 问题,但能避免最典型的 false sharing。
7. 局部归约优于全局原子
坏模式:
1 | std::atomic<double> global_sum{0.0}; |
即使类型支持原子加,这也往往很慢。
所有线程都争一个位置,cache line 会在核心和 socket 之间来回迁移。
更好的模式:
1 | std::vector<double> partial(num_threads, 0.0); |
NUMA 机器上还可以再进一步:
1 | 线程本地归约 |
8. 线程亲和性是什么
线程亲和性是把线程尽量固定在某些 CPU core 上运行。
如果线程一直搬家,会带来:
- cache 热数据失效
- first touch 布局和执行位置不匹配
- benchmark 波动更大
亲和性不是所有程序都要手动设置。
但在多路 CPU、高吞吐、低延迟系统里,它很常见。
常见工具或 API:
numactltasksetpthread_setaffinity_npsched_setaffinity- hwloc
- TBB
task_arena和 affinity 相关机制
注意:
- 绑定过死可能影响系统调度
- 容器和虚拟机环境里 CPU 拓扑可能被隐藏
- benchmark 环境和生产环境要区分
9. 用 numactl 观察和控制
Linux 上常用:
1 | numactl --hardware |
查看 NUMA node、CPU 和内存分布。
只在某个 node 上运行:
1 | numactl --cpunodebind=0 --membind=0 ./app |
交错分配内存:
1 | numactl --interleave=all ./app |
这些命令适合做实验:
- 绑定同一 node
- 故意绑定远端内存
- 交错分配
- 比较不同策略的带宽和延迟
如果不同策略差异很大,说明程序对 NUMA 很敏感。
10. 常用观察工具
10.1 lscpu
1 | lscpu |
可以看:
- socket 数
- core 数
- thread 数
- NUMA node
10.2 numastat
1 | numastat |
可以观察进程内存分布和远端访问线索。
10.3 hwloc
1 | lstopo |
可以看更完整的硬件拓扑:
- NUMA node
- socket
- core
- cache 层级
- PCIe 设备
10.4 perf c2c
perf c2c 可以帮助分析 cache line 争用。
它对定位 false sharing 和跨核 cache line 迁移很有用。
11. NUMA 和内存分配器
内存分配器也会影响 NUMA。
关注点:
- 分配发生在哪个线程
- 初始化发生在哪个线程
- 对象之后主要由哪个线程使用
- 是否有全局 allocator 锁
- 是否使用线程本地 cache
工程里常见策略:
- 每个线程或每个 socket 使用本地 arena
- 避免一个线程分配、另一个 socket 大量使用
- 大数组并行初始化
- 热对象不要在 socket 间频繁转移所有权
这和 std::pmr、内存池、线程本地缓存都能接起来。
12. NUMA 和任务调度
任务调度器喜欢负载均衡。
NUMA 喜欢数据本地性。
这两者有时会冲突。
例如 work stealing 可以让空闲线程偷任务,提高 CPU 利用率。
但如果偷来的任务访问远端 node 的大数据,可能导致远端访存增加。
工程上常见折中:
- 大任务先按 NUMA node 分区
- node 内部再做 work stealing
- 小结果最后跨 node 合并
- 对内存带宽型任务减少跨 node 迁移
TBB 这类调度器已经做了很多局部性优化,但仍需要你给出合理的数据划分和任务粒度。
13. 哪些程序最容易受 NUMA 影响
高风险场景:
- 大数组扫描
- 稀疏矩阵
- 图算法
- 数据库 / KV 存储
- 大规模仿真
- 日志/网络包批处理
- 高频共享队列
- 多线程内存分配密集程序
低风险场景:
- 数据很小
- 主要瓶颈是 I/O
- 单 socket 内运行
- 线程数不高
- 工作集大多在 cache 中
判断标准很朴素:
如果程序已经被内存带宽或 cache coherence 限制,NUMA 很可能重要。
14. 一个简单的 NUMA 排查流程
确认机器拓扑
lscpu、numactl --hardware、lstopo测单线程和多线程扩展性
看跨 socket 后是否突然变差。测不同绑定策略
同 node、跨 node、interleave。检查内存初始化
是否单线程 first touch。检查共享写
是否有全局原子、锁、队列、计数器。改成分区处理
本地初始化、本地计算、本地归约。复测
看吞吐、延迟和波动是否改善。
15. 常见误区
15.1 “多线程慢就是锁的问题”
不一定。
跨 socket 远端访存和 cache line 迁移也会让程序很慢。
15.2 “内存分配在哪里不重要”
在 NUMA 上很重要。
分配和 first touch 可能决定页面位置。
15.3 “线程越均匀越好”
计算量均匀还不够。
数据位置也要尽量匹配。
15.4 “interleave 一定最好”
不一定。
交错分配能平摊带宽,但也可能破坏本地性。
15.5 “NUMA 只和服务器有关”
主要出现在多路服务器,但任何有明显内存拓扑差异的系统都值得留意。
只是普通桌面小程序通常不需要先处理它。
16. 一页总结
NUMA 的核心理解链:
- 不同 CPU 到不同内存的距离可能不同
- first touch 会影响页面放在哪里
- 线程位置和数据位置不匹配会导致远端访存
- 跨 socket 共享 cache line 很贵
- 本地分区、本地初始化、本地归约通常是第一解
- 绑定和工具只是验证手段,数据布局才是长期方案
一句话:
NUMA 优化的核心不是“绑核”,而是让计算尽量发生在数据旁边。
17. 建议继续补充的相关主题
和本篇衔接最紧密的内容:
- Linux
numactl/numastat实战 - hwloc 拓扑建模
- TBB task arena 与 NUMA-aware 调度
- 多 socket 上的内存带宽 benchmark
- 数据库和图计算中的 NUMA 分区设计
18. 参考资料
Linux numactl
https://github.com/numactl/numactlIntel Optimization Manual
https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html