1. 实验背景

本文基于一台 Dell T430 双路服务器,围绕 Linux NUMA(Non-Uniform Memory Access,非一致内存访问)机制进行了基础验证实验。实验目标主要包括:

  1. 识别并确认服务器的 NUMA 拓扑。
  2. 理解 CPU 绑核与内存绑定之间的区别。
  3. 验证本地内存访问(local access)与远端内存访问(remote access)的性能差异。
  4. 验证 Linux 默认的 first-touch 内存分配行为。
  5. 初步观察 automatic NUMA balancing 与 interleave 分配策略的影响。

本文“基于实测数据得出结论”。


2. 实验平台

2.1 服务器硬件概况

  • 服务器型号:Dell T430
  • CPU:2 × Intel Xeon E5-2682 v4 @ 2.50GHz
  • 架构:x86_64
  • 每颗 CPU:
    • 16 个物理核
    • 32 个逻辑 CPU(开启超线程)
  • 全机总逻辑 CPU 数:64
  • 内存总量:约 62 GiB
  • NUMA node 数量:2

2.2 NUMA 拓扑

numactl --hardware 可见:

  • node 0:
    • CPU:0 2 4 … 62
    • 内存:31703 MB
  • node 1:
    • CPU:1 3 5 … 63
    • 内存:32202 MB

节点距离矩阵如下:

Node 0 1
0 10 21
1 21 10

这说明:

  • 本地访问代价较低(distance=10)
  • 跨节点访问代价明显更高(distance=21)

这正是 NUMA 的典型特征:内存访问代价取决于 CPU 与内存是否处于同一 NUMA node。


3. 拓扑识别实验

3.1 numactl --hardware

通过 numactl --hardware,可以快速确认:

  • 系统存在 2 个 NUMA node
  • 每个 node 对应一组独立 CPU
  • 每个 node 具备独立本地内存容量
  • 节点间存在本地/远端距离差异

这是 NUMA 实验的第一步,也是后续绑核与绑内存实验的基础。

3.2 lstopo-no-graphics

lstopo-no-graphics 进一步揭示了更细粒度的硬件结构:

  • 每个 Package 对应一个 NUMA node
  • 每个 Package 下有独立的 L3 cache(40 MB)
  • 每个 Core 下挂两个 PU(Processing Unit),对应超线程
  • NUMA node 与 CPU package 高度对应

由此可确认本机属于典型双路双 NUMA 服务器,非常适合做本地/远端访存差异实验。

3.3 sysfs 验证

通过以下目录可直接从内核视角观察 NUMA 结构:

  • /sys/devices/system/node/node0
  • /sys/devices/system/node/node1

其中:

  • cpulist 可查看 node 内 CPU 列表
  • meminfo 可查看 node 内存总量与剩余量
  • distance 可查看 node 距离
  • numastat / vmstat 可查看 node 级别统计信息

这说明 Linux 内核已经完整暴露 NUMA 拓扑,用户态可以方便地基于 sysfs 做自动化识别。


4. 内存硬件分布观察

4.1 按 node 统计的内存容量

实验记录显示:

  • node0:32464060 kB
  • node1:32975788 kB

两边容量接近,各自约 31 GiB,符合双路均匀分布预期。

4.2 内存 block 大小

/sys/devices/system/memory/block_size_bytes 的值为:

1
80000000

即十六进制 0x80000000,折合 2 GiB。

这表示 Linux 内核在该平台上以内存 section / block 的粒度管理热插拔与节点归属信息。对于本文主题而言,这不是性能核心因素,但有助于理解 /sys/devices/system/node/nodeX/memory* 目录为何按离散 block 展示。

4.3 DIMM 信息

dmidecode -t memory 显示:

  • A1:32 GB DDR4 RDIMM,2400 MT/s
  • B1:32 GB DDR4 RDIMM,2400 MT/s

其余插槽为空。

这意味着当前服务器总内存由两条 32 GB 内存条组成,分布在两个 CPU 对应的内存通道侧。虽然详细通道平衡信息未进一步展开,但从 node0 / node1 内存容量接近可以看出,OS 已将其识别为两侧近似对称的 NUMA 内存。


5. CPU 绑核与内存绑定实验

本节核心目的,是区分以下两个概念:

  • CPU affinity / cpubind:进程在哪些 CPU 上运行
  • Memory policy / membind:进程从哪些 NUMA node 分配内存

5.1 仅绑 CPU,不绑内存

执行:

1
numactl --cpunodebind=0 --show

输出显示:

  • cpubind: 0
  • nodebind: 0
  • membind: 0 1
  • policy: default

这说明:

  • 进程运行位置被限制在 node0 的 CPU 上
  • 但内存仍可从 node0 和 node1 分配
  • 仅绑 CPU,并不等于内存自动绑定到同一 node

同样地,执行:

1
2
numactl --cpunodebind=1 bash
numactl --show

可观察到:

  • cpubind: 1
  • membind: 0 1

结论完全一致。

5.2 同时绑定 CPU 与本地内存

执行:

1
2
numactl --cpunodebind=0 --membind=0 bash
numactl --show

可见:

  • policy: bind
  • cpubind: 0
  • membind: 0

这表示:

  • 进程只在 node0 的 CPU 上运行
  • 进程内存只从 node0 分配

这是最标准的“本地执行、本地分配”。

5.3 故意制造远端访存

执行:

1
2
numactl --cpunodebind=0 --membind=1 bash
numactl --show

可见:

  • cpubind: 0
  • membind: 1

这意味着:

  • 进程跑在 node0 CPU
  • 但内存强制从 node1 分配

这类配置是构造 NUMA 反例实验的常见方法,也正是后续性能对比的基础。


6. Benchmark 程序设计

实验中编译了一个自定义测试程序 numa_walk.c,并生成可执行文件 numa_walk

程序逻辑概括如下:

  1. 分配指定大小的内存(本实验使用 16 GiB)。
  2. 通过按页写入完成 first-touch,确保页真正落地到物理内存。
  3. 顺序遍历缓冲区并累加,统计总耗时。
  4. 计算带宽(GiB/s)。

这种设计非常适合 NUMA 入门验证,因为它能放大以下影响:

  • 内存页实际落在哪个 node
  • 当前线程跑在哪个 node
  • 本地访问与远端访问之间的差异

7. 本地访问与远端访问性能对比

7.1 实验命令

共测试了四组:

1
2
3
4
numactl --cpunodebind=0 --membind=0 ./numa_walk 16 20
numactl --cpunodebind=0 --membind=1 ./numa_walk 16 20
numactl --cpunodebind=1 --membind=1 ./numa_walk 16 20
numactl --cpunodebind=1 --membind=0 ./numa_walk 16 20

7.2 实测结果

编号 CPU 所在 node 内存所在 node 类型 耗时(s) 带宽(GiB/s)
A 0 0 本地访问 27.989 11.43
B 0 1 远端访问 46.580 6.87
C 1 1 本地访问 28.039 11.41
D 1 0 远端访问 45.313 7.06

7.3 结果分析

7.3.1 本地访问性能明显更好

两组本地访问结果非常接近:

  • node0 本地:11.43 GiB/s
  • node1 本地:11.41 GiB/s

说明两侧拓扑较为对称,且在该工作负载下,本地内存读取性能稳定。

7.3.2 远端访问性能明显下降

两组远端访问分别为:

  • node0 CPU 访问 node1 内存:6.87 GiB/s
  • node1 CPU 访问 node0 内存:7.06 GiB/s

相比本地访问,带宽下降约:

  • B 相对 A:下降约 39.9%
  • D 相对 C:下降约 38.1%

可见在该机器上,远端访存带来的性能损失接近 40%。

7.3.3 结论

对该类流式内存遍历负载而言:

NUMA locality 是决定性能的关键因素之一。

只要 CPU 与内存不在同一 NUMA node,即使代码完全相同,也会出现显著性能下滑。


8. first-touch 验证实验

Linux 默认内存分配策略中,一个非常关键的原则是:

页通常会被分配到“首次真正写入该页的 CPU 所属 node”。

这就是 first-touch。

8.1 在 node0 上 first-touch

执行:

1
2
3
numactl --cpunodebind=0 ./numa_walk 16 5 10 &
numastat -p $PID
cat /proc/$PID/numa_maps | head -n 30

在程序完成 first-touch 后,可见:

  • numastat -p 显示约 16385 MB Private 内存位于 Node 0
  • Node 1 仅有极少量页
  • numa_maps 中大块匿名内存显示为 N0=4194305

这说明 16 GiB 主体数据页几乎全部分配在 node0。

8.2 在 node1 上 first-touch

执行:

1
2
3
numactl --cpunodebind=1 ./numa_walk 16 5 10 &
numastat -p $PID
cat /proc/$PID/numa_maps | head -n 30

在程序完成 first-touch 后,可见:

  • numastat -p 显示约 16384 MB Private 内存位于 Node 1
  • Node 0 仅有约 1.41 MB 零星页
  • numa_maps 中大块匿名内存显示为 N1=4194305

这说明 16 GiB 主体数据页几乎全部落在 node1。

8.3 结论

该实验直接验证了:

在默认策略下,Linux 会将绝大多数匿名页分配到首次触碰它们的 CPU 所属 NUMA node。

这也是 NUMA 优化中最常见、最重要的基础知识之一。很多应用没有显式使用 numactl 或 NUMA API,但其实际页布局已经深受 first-touch 影响。


9. automatic NUMA balancing 初步观察

9.1 当前状态

实验前查看:

1
cat /proc/sys/kernel/numa_balancing

结果为:

1
1

说明 automatic NUMA balancing 默认开启。

随后执行:

1
echo 0 > /proc/sys/kernel/numa_balancing

将其关闭。

9.2 实验过程

先关闭 automatic NUMA balancing:

1
echo 0 > /proc/sys/kernel/numa_balancing

随后执行:

1
2
3
4
5
taskset -c 0 ./numa_walk 16 50 15 &
PID=$!
sleep 3
taskset -cp 1,3,5,7 $PID
watch -n 1 "numastat -p $PID"

实验含义如下:

  1. 先让程序在 CPU0 上运行并完成 first-touch。
  2. 由于 first-touch 发生在 node0,主体数据页优先落在 node0。
  3. 随后将进程的 CPU affinity 迁移到 node1 的 CPU 集合。
  4. 在 automatic NUMA balancing 关闭的情况下,观察页是否仍主要停留在 node0。

在 automatic NUMA balancing 关闭时,numastat -p 观察到:

1
2
3
4
5
6
7
8
9
Per-node process memory usage (in MBs) for PID 598730 (numa_walk)
                           Node 0          Node 1           Total
                  --------------- --------------- ---------------
Huge                         0.00            0.00            0.00
Heap                         0.00            0.00            0.00
Stack                        0.02            0.00            0.02
Private                  16384.66            0.83        16385.49
----------------  --------------- --------------- ---------------
Total                    16384.68            0.83        16385.51

这说明:

  • 进程执行位置已经迁移到 node1 的 CPU 集合;
  • 但绝大多数 Private 匿名页仍停留在 node0;
  • 因而形成了非常典型的 CPU 在 node1、内存在 node0 的 remote access 场景。

随后,在另一个终端重新开启 automatic NUMA balancing:

1
echo 1 > /proc/sys/kernel/numa_balancing

继续观察 numastat -p,可见页分布逐步迁移到 node1。实验记录中的一个观察点如下:

1
2
3
4
5
6
7
8
9
Per-node process memory usage (in MBs) for PID 598730 (numa_walk)
                           Node 0          Node 1           Total
                  --------------- --------------- ---------------
Huge                         0.00            0.00            0.00
Heap                         0.00            0.00            0.00
Stack                        0.01            0.00            0.02
Private                      0.66        16384.83        16385.49
----------------  --------------- --------------- ---------------
Total                        0.67        16384.84        16385.51

这说明在重新开启 automatic NUMA balancing 后:

  • 大量原先位于 node0 的匿名页被逐步迁移到 node1;
  • 迁移后的页分布已经从“几乎全在 node0”变为“几乎全在 node1”;
  • Linux 内核确实在尝试恢复线程执行位置与页位置之间的 locality。

9.3 结果分析与结论

这一组实验已经形成较完整的证据链:

  1. 进程最初在 node0 上 first-touch,因此大部分页首先落在 node0。
  2. 进程随后被迁移到 node1 的 CPU 上运行。
  3. 当 automatic NUMA balancing 关闭时,numastat -p 明确显示页仍主要停留在 node0,形成 remote access。
  4. 当重新开启 automatic NUMA balancing 后,numastat -p 明确显示页从 node0 逐步迁移到 node1。

因此,本实验可以得出较强结论:

automatic NUMA balancing 能够根据任务的实际执行位置,对匿名页进行跨 NUMA node 迁移,以改善内存 locality。

同时,这一实验也进一步说明:

  • NUMA 性能问题并不只取决于“程序起初在哪分配内存”,
  • 还取决于“程序运行过程中线程是否迁移、内核是否允许并完成页迁移”。

从性能结果看,迁移场景下的测得带宽为:

  • time=94.216 s
  • bandwidth=8.49 GiB/s

该值低于严格本地访问(约 11.4 GiB/s),但高于严格远端绑定(约 6.9~7.1 GiB/s)。这与实验现象是一致的:任务执行过程中 locality 先变差,随后在 automatic NUMA balancing 开启后逐步恢复,因此整体性能处于二者之间。

9.4 本节小结

本节实验清楚展示了三种状态:

  1. first-touch 后页落在 node0;
  2. CPU 迁移到 node1、但页仍留在 node0 时,形成 remote access;
  3. 重新开启 automatic NUMA balancing 后,页会逐步迁移到 node1,恢复 locality。

这使得 NUMA 实验不再只是“静态绑核/绑内存”的验证,而是进一步观察到了 Linux 内核在动态运行阶段对 NUMA locality 的修正能力。

10. interleave 策略实验

10.1 实验命令

执行:

1
numactl --cpunodebind=0 --interleave=all ./numa_walk 16 20

得到结果:

  • 耗时:37.229 s
  • 带宽:8.60 GiB/s

随后再次复测对照组:

1
2
numactl --cpunodebind=0 --membind=0 ./numa_walk 16 20
numactl --cpunodebind=0 --membind=1 ./numa_walk 16 20

结果为:

  • 本地:11.31 GiB/s
  • 远端:6.89 GiB/s

10.2 对比结果

策略 CPU node 内存策略 带宽(GiB/s)
本地绑定 0 --membind=0 11.31
交错分配 0 --interleave=all 8.60
远端绑定 0 --membind=1 6.89

10.3 结论

interleave=all 的性能落在本地与远端之间,符合预期。

其原因在于:

  • 一部分页落在 node0,本地访问较快
  • 另一部分页落在 node1,访问时需要跨 node
  • 因此整体性能被“平均化”

这类策略的价值不在于追求单线程极致性能,而更适用于:

  • 需要跨 node 均匀消耗内存容量
  • 希望降低单 node 内存压力
  • 负载对绝对延迟不那么敏感的场景

11. 实验结论

基于本次实测,可以得到以下结论。

11.1 本机是典型双路双 NUMA 服务器

该 Dell T430 具备:

  • 2 个 CPU package
  • 2 个 NUMA node
  • 每个 node 约 31 GiB 本地内存
  • 本地/远端 node distance 分别为 10 / 21

这为 NUMA locality 研究提供了非常清晰的平台。

11.2 仅绑 CPU 不等于绑内存

--cpunodebind 只能限制进程在哪些 CPU 上运行,默认并不会将内存同步绑定到同一 node。若要保证内存本地化,需要显式指定 --membind

11.3 本地访问显著优于远端访问

在本实验的 16 GiB 顺序遍历负载中:

  • 本地访问约 11.4 GiB/s
  • 远端访问约 6.9~7.1 GiB/s

远端访存性能下降约 38%~40%。

这说明 NUMA 亲和性对内存密集型程序影响非常显著。

11.4 first-touch 行为被明确验证

在默认策略下:

  • 进程在 node0 上 first-touch,则大页主要落在 node0
  • 进程在 node1 上 first-touch,则大页主要落在 node1

这说明很多程序即便没有主动做 NUMA 优化,其实际页布局也已经被初始化阶段的线程位置决定。

11.5 interleave 策略是折中方案

--interleave=all 的性能处于本地绑定与远端绑定之间,更适合容量均衡,而非极致性能。

11.6 automatic NUMA balancing 可在运行期修正 NUMA locality

本次实验已经验证:

  • automatic NUMA balancing 默认开启;
  • 关闭该机制后,进程从 node0 迁移到 node1 运行时,主体页仍停留在 node0;
  • 重新开启该机制后,numastat -p 明确显示主体页从 node0 逐步迁移到 node1。

这说明 Linux 内核能够根据线程的实际执行位置,对匿名页执行跨 node 迁移,从而改善 locality。

更准确地说:

当线程运行位置与页位置发生偏离时,automatic NUMA balancing 可以在运行期逐步把热点页迁移到更合适的 NUMA node。


12. 对 EDA / HPC 场景的启发

对于 EDA、仿真、版图、签核、数据库、HPC 这类大内存工作负载,NUMA 往往不是“理论知识”,而是直接影响生产性能的关键因素。

12.1 对单进程大内存程序

若线程主要运行在一个 socket,但数据页落在另一个 socket,则会长期承受 remote access penalty,表现为:

  • 带宽下降
  • 延迟上升
  • CPU 利用率看似正常但任务耗时偏长

12.2 对多线程程序

如果初始化阶段全部线程集中在一个 node 完成 first-touch,而后续计算线程分布到两个 node,则很容易造成一半线程长期访问远端页。

12.3 对调度系统

在 LSF / Slurm 等调度环境中,若调度器只关心“给了多少核”和“给了多少内存”,而不关心 CPU 与内存的 node 亲和关系,则用户侧会看到:

  • 资源配额足够
  • 但任务性能不稳定或偏低

因此 NUMA 感知调度、绑核与内存本地化策略,都是高性能计算环境中的基础能力。


13. 后续建议实验

为了进一步掌握 NUMA,建议继续补做以下实验:

13.1 多线程版本 benchmark

当前 numa_walk 更接近单线程流式访问模型。下一步可扩展为:

  • 2 线程 / 4 线程 / 8 线程版本
  • 分别比较“线程与数据局部性良好”和“线程与数据局部性打乱”的差异

13.2 自动页迁移量化实验

本次已经观察到:

  • numa_balancing=0 时,CPU 迁到 node1 后,页仍主要停留在 node0;
  • numa_balancing=1 重新开启后,页会逐步迁移到 node1。

后续可进一步做量化增强:

  • 固定采样时间点(例如 0s / 5s / 10s / 20s)
  • 每个时间点记录 numastat -p
  • 同时保留 /proc/<pid>/numa_maps 中热点匿名段的 node 分布
  • 统计迁移完成时间与性能恢复速度

这样可以把“观察到迁移”进一步提升为“定量分析迁移速度与收益”。

13.3 使用 perf 观察硬件事件

可进一步引入:

1
perf stat -d -d -d <command>

观察:

  • cache misses
  • stalled cycles
  • instructions per cycle

这样能把 NUMA 带来的性能退化与微架构指标联系起来。

13.4 使用 numastat 观察系统级行为

除了 numastat -p PID,还可以直接运行:

1
numastat

观察系统层面的:

  • local_node
  • other_node
  • numa_hit
  • numa_miss
  • interleave_hit

从全局视角理解 NUMA 访问情况。


14. 总结

本次实验已经完成了 NUMA 学习中最核心的一步:把抽象概念转化为可重复、可量化、可解释的实测结果。

最重要的收获有三点:

  1. NUMA 不是“有或没有”的概念,而是“线程与页是否匹配”的问题。
  2. first-touch 会直接影响页落点,是理解 NUMA 的核心入口。
  3. 本地访存与远端访存的性能差异是真实且显著的,在本机上可达到约 40% 的带宽差距。
  4. automatic NUMA balancing 不只是一个开关名词,而是能够在运行过程中把热点页从远端 node 迁回当前执行 node 的实际机制。

对于后续的性能优化、EDA 平台调优、HPC 调度设计而言,这些结论已经足以作为第一层实践基础。


15. 附:关键实验命令清单

查看 NUMA 拓扑

1
2
3
4
5
6
7
numactl --hardware
lstopo-no-graphics
for n in /sys/devices/system/node/node*; do
    echo "===== $n ====="
    cat $n/cpulist
    egrep 'MemTotal|MemFree' $n/meminfo
done

查看当前进程 NUMA 策略

1
2
3
4
numactl --show
numactl --cpunodebind=0 --show
numactl --cpunodebind=0 --membind=0 --show
numactl --cpunodebind=0 --membind=1 --show

运行本地 / 远端对比实验

1
2
3
4
numactl --cpunodebind=0 --membind=0 ./numa_walk 16 20
numactl --cpunodebind=0 --membind=1 ./numa_walk 16 20
numactl --cpunodebind=1 --membind=1 ./numa_walk 16 20
numactl --cpunodebind=1 --membind=0 ./numa_walk 16 20

观察 first-touch

1
2
3
numactl --cpunodebind=0 ./numa_walk 16 5 10 &
numastat -p $PID
cat /proc/$PID/numa_maps | head -n 30

观察 automatic NUMA balancing

1
2
3
4
5
cat /proc/sys/kernel/numa_balancing
echo 0 > /proc/sys/kernel/numa_balancing
taskset -c 0 ./numa_walk 16 50 15 &
taskset -cp 1,3,5,7 $PID
watch -n 1 "numastat -p $PID"

interleave 策略实验

1
numactl --cpunodebind=0 --interleave=all ./numa_walk 16 20