项目
实验-CPU 核心数增加但程序更慢
项目源码
https://github.com/zhanghao0806/cpu_counter_experiment
研究问题
问题1 为什么 CPU 线程数越多,但是程序不一定越快?
原因:增加线程可以把计算分摊出去,但线程之间的相互干扰也可能增加,导致整体时间不一定更快。线程的相互干扰是什么下面有提到。
用计数器模拟总任务。计数器累计到 12 亿次,任务结束。
问题2 相同的多线程下,不同的内存布局,会如何影响多线程性能?
不同的内存布局是指计数器存放位置:是否在一个缓存行。
实验方案
实验一
目的:验证问题 1
在 Global(下面提到的 3 种计数器更新方式的一种)条件下,统计用 6 档线程数(1、2、4、8、16、32)完成全部任务的耗时,以及相对单线程的加速比。
实验二
目的:验证问题 2
在相同的线程数量(1、2、4、8、16、32)下,分别在Compact、Padded 条件下,统计任务耗时。
总工作量为 12 亿次,每种配置重复 10 次,用中位数比较。项目同时保留了全部原始数据。
计数器三种更新方式
Global
多个线程共同修改一个全局变量。
可能的硬件开销:线程 A 修改计数器时,需要获得写入权限,权限的传递需要代价;A 修改了计数器,B 缓存的有计数器的副本,硬件需要让 B 的副本失效,需要代价;
Compact
没有全局计数器。线程有自己的计数器,多个线程的计数器之和等于 12 亿,任务完成。
和 Padded 区别:计数器在内存中连续存放,每组最多 8 个计数器位于同一个 64 B 缓存行;16、32 线程分别使用 2、4 条缓存行。
CPU 按缓存行维护缓存一致性,因此各线程虽然写的是自己的计数器,但是仍可能互相干扰。
对缓存行的竞争,带来的开销和 Global 中修改全局变量类似。
怎么保证计数器在放在一个缓存行?
服务器的缓存行大小 64 字节。计数器大小 8 字节。
起点对齐:alignas(64) 保证每组的起始地址是 64 的整数倍:缓存行边界。
组内连续:CompactLine 内部的原子计数器数组包含 8 个连续元素,外部 vector<CompactLine> 连续存放这些对齐的组;组内 8 个计数器连续存放,起始偏移分别为 0、8、16、24、32、40、48、56 字节,恰好占满这一行。
Padded
没有全局计数器。线程有自己的计数器,所有工作线程的计数器之和等于 12 亿,任务完成。
和 Compact 区别:各计数器在内存中隔开存放。和 Compact 相比,减少了缓存行竞争带来的开销。
怎么保证计数器不在同一个缓存行?
把每个计数器包装成一个大小为 64 字节、起始地址按 64 字节对齐的对象。
实验结果
原始数据在仓库文件里。
这里简单介绍一些实验结果。
第一个问题:为什么线程更多,程序不一定更快?
Global 实验数据如下:
| 工作线程数 | 耗时中位数 |
|---|---|
| 1 | 1.591 秒 |
| 2 | 4.127 秒 |
| 4 | 4.289 秒 |
| 8 | 4.380 秒 |
| 16 | 6.317 秒 |
| 32 | 6.437 秒 |
所有配置都处理同样的 12 亿次总任务,但 32 线程耗时约为单线程的 4.05 倍。这直接说明:更多工作线程并不保证更高性能。
计算可以分配到多个核心,但对这个共享位置的更新仍需要协调;结果与“核心之间的竞争和缓存一致性开销抵消了并行收益”的解释一致,但本次没有直接用硬件性能计数器分解各项开销。
第二个问题:相同线程数,不同内存布局会如何影响性能?
| 工作线程数 | Compact 中位数 | Padded 中位数 | Padded 加速倍数 |
|---|---|---|---|
| 1 | 1866.89 ms | 1878.10 ms | 0.99 倍 |
| 2 | 3596.31 ms | 939.24 ms | 3.83 倍 |
| 4 | 4624.14 ms | 473.76 ms | 9.76 倍 |
| 8 | 4216.73 ms | 239.05 ms | 17.64 倍 |
| 16 | 2106.15 ms | 120.86 ms | 17.43 倍 |
| 32 | 1085.39 ms | 68.26 ms | 15.90 倍 |
单线程时几乎没有差别:没有多个核心相互干扰,隔开计数器没有明显收益。
多线程时差距明显:Compact 每 8 个计数器共享一条缓存行;Padded 每个计数器独占一行。隔离布局显著改善了性能。
实验结果分析
不同的内存布局是怎么影响多线程性能的?
详细的解释见仓库PPT:计算系统跨层优化 Presentation
这里有做实验的 CPU 的缓存架构图,帮助理解多级 cache:CPU 缓存示意图
实验服务器参数
| 项目 | 实测配置 |
|---|---|
| CPU | AMD Ryzen Threadripper PRO 7975WX 32-Cores |
| 架构 | x86_64 |
| CPU 插槽 / 物理核心 / 逻辑线程 | 1 / 32 / 64;每核 2 个 SMT 线程 |
| 在线及当前进程可用 CPU | 0–63;nproc = 64,进程亲和性为 0–63 |
| NUMA | 1 个节点,node 0 包含 CPU 0–63 |
| 缓存行大小 | 64 B;sysfs 中各级缓存均为 64 B |
| L1 数据缓存 | 每核 32 KiB,共 32 个实例,总计 1 MiB |
| L1 指令缓存 | 每核 32 KiB,共 32 个实例,总计 1 MiB |
| L2 缓存 | 每核 1 MiB,共 32 个实例,总计 32 MiB |
| L3 缓存 | 每组 32 MiB,共 4 个实例,总计 128 MiB;每组由 8 个物理核心共享 |
| 内存 | 系统可见约 251.2 GiB(MemTotal: 263374732 kB) |
| 操作系统 | Ubuntu 24.04 LTS |
| 内核 | Linux 7.0.0-38-generic,x86_64 |
| CPU 调频 | CPU 0 的 governor 为 powersave; |
CPU 0–31 对应 32 个不同物理核心;CPU 32–63 是其 SMT 同胞。CPU i 与 CPU i+32 共享同一物理核心(i 为 0–31)。