简介:本资源是Linux系统内存性能基准测试工具STREAM的完整源码包,面向系统管理员、性能工程师及底层开发人员,用于精准评估服务器或工作站的内存带宽与数据吞吐能力,尤其适用于大数据分析、科学计算等对内存子系统敏感的场景。压缩包为17KB的ZIP格式,共8个文件:含核心实现文件stream.c与mysecond.c(C语言主逻辑)、stream.f(Fortran版本)、Makefile(一键编译支持)、README与HISTORY.txt(使用说明与演进记录)、.gitignore及LICENSE.txt,结构精简、开箱即用。目前已有2973人学习下载,体现了其在性能调优领域的广泛实践基础。用户可直接编译运行获取Copy/Scale/Add/Triad四类内存操作的实测带宽(MB/s),结合源码深入理解内存访问模式与缓存行为,同时为跨平台对比、硬件选型及内核参数调优提供可靠基准依据。
1. Stream 不是“流媒体”,而是内存带宽的照妖镜:为什么你的高性能计算节点跑不满标称带宽?
你刚部署完一台搭载 DDR5-4800 的双路服务器,理论内存带宽标称 153.6 GB/s,但跑完 HPL、HPCG 等标准基准后,实际应用吞吐总卡在 90–110 GB/s 区间,GPU 显存带宽跑得飞起,CPU 内存却像被掐住脖子——这时候,别急着怀疑 BIOS 设置或 NUMA 绑定,先用stream跑一发。它不测延迟、不压缓存、不模拟业务逻辑,就干一件事:用最朴素的 C 数组循环,榨干 CPU 到内存控制器之间的物理通路极限。很多团队直到上线半年后才第一次运行stream,结果发现——标称带宽根本没被触达,不是代码写得差,是硬件链路本身存在隐性瓶颈:内存插槽配对错误、CL 时序未收敛、IMC(集成内存控制器)电压不足、甚至主板 PCB 走线阻抗失配。stream就是那个不讲情面的“内存带宽体检仪”:它不关心你用的是 OpenMP 还是 MPI,只认地址对齐、访存模式、编译器向量化能力这三样硬指标。适合所有做 HPC、AI 训练底座调优、数据库内存密集型负载部署的一线工程师;新手靠它建立对“真实带宽”的直觉,老手靠它快速定位硬件交付异常。它不是万能的,但凡stream跑不出 85% 以上标称值,后续所有上层性能优化大概率是在修漏水的桶。
2. 从源码编译到四核并行:用 stream 在本地跑通内存带宽测试的最小命令
2.1 下载与编译:为什么必须自己编译,而不是用包管理器安装?
stream官方源码(由 John D. McCalpin 博士维护)长期托管在 https://www.cs.virginia.edu/stream/ ,最新稳定版为stream_v5.10.c(截至 2024 年中)。切勿使用apt install stream或yum install stream安装的版本——这些包通常来自第三方仓,编译参数固化(如固定-O2、禁用 AVX-512、未定义STREAM_ARRAY_SIZE),且无法控制线程绑定策略,测出来的是“发行版妥协后的带宽”,不是你机器的真实能力。
我们采用手动编译,核心目标有三个:
① 启用最高级向量化(AVX2/AVX-512);
② 控制数组大小,确保完全驻留于主存(避开 L3 缓存干扰);
③ 绑定到指定 CPU 核心,排除调度抖动。
# 创建工作目录并下载源码(注意:必须用 curl -L 获取重定向后的真实地址) mkdir -p ~/stream-test && cd ~/stream-test curl -L -O https://www.cs.virginia.edu/stream/FTP/Code/stream.c # 重命名为更清晰的文件名,并设置关键宏定义 mv stream.c stream_v5.10.c # 编译命令(关键参数逐个说明) gcc -O3 -march=native -fopenmp -DSTREAM_ARRAY_SIZE=200000000 \ -DNTIMES=10 -DSTREAM_TYPE=float stream_v5.10.c -o stream_omp_float参数说明:
-O3:启用激进优化,对循环展开和向量化至关重要;-march=native:让编译器自动识别当前 CPU 支持的指令集(自动启用 AVX2 或 AVX-512);-fopenmp:启用 OpenMP 并行,否则默认单线程(测的是峰值带宽的 1/N);-DSTREAM_ARRAY_SIZE=200000000:定义数组长度为 2 亿个 float 元素 → 占用内存 =2e8 × 4B = 800 MB,远大于典型 L3 缓存(如 64MB),确保强制走 DRAM;-DNTIMES=10:每个测试项重复 10 次,取后 5 次平均值(前 5 次用于预热缓存与 TLB);-DSTREAM_TYPE=float:使用 float 类型(比 double 更友好,减少数据搬运量,更易打满带宽)。
编译成功后,执行./stream_omp_float,你会看到类似输出:
------------------------------------------------------------- STREAM version $Revision: 5.10 $ ------------------------------------------------------------- This system uses 8 bytes per array element. Array size = 200000000 (elements), Offset = 0 (elements) Memory per array = 1525.879 (MiB) ≈ 1.5 GB Total memory required = 4577.637 (MiB) ≈ 4.5 GB Each kernel will be executed 10 times. The *best* time for each kernel (excluding the first iteration) will be used to compute the reported bandwidth. ------------------------------------------------------------- Number of Threads requested = 8 Number of Threads counted = 8 ------------------------------------------------------------- Your clock granularity/precision appears to be 1 microseconds. Each test below will take on the order of 1000000 microseconds (1 sec). (= 1000000 iterations) ------------------------------------------------------------- Function Best Rate MB/s Avg time Min time Max time Copy: 124521.3 0.012880 0.012850 0.012920 Scale: 123890.1 0.012930 0.012900 0.012970 Add: 131255.7 0.019320 0.019280 0.019370 Triad: 130722.4 0.019400 0.019350 0.019450 ------------------------------------------------------------- Peak memory bandwidth (based on total copy + scale + add + triad): 130122.4 MB/s -------------------------------------------------------------注意看Number of Threads counted = 8—— 这表示 OpenMP 成功识别了全部 8 个逻辑核。若显示1,说明-fopenmp未生效或环境变量OMP_NUM_THREADS被设为 1。
2.2 四核绑定实测:用 taskset 锁定物理核心,排除超线程干扰
多核共享内存控制器,但超线程(SMT)线程共用同一套执行单元和内存请求队列。若用全部逻辑核(如 32 线程)跑stream,可能因资源争抢导致带宽反而下降。最佳实践是绑定到物理核心(而非逻辑线程),尤其在双路系统中需明确跨 NUMA 节点行为。
假设你的 CPU 是 8 核 16 线程(Intel i9-13900K 或 AMD Ryzen 7 7800X3D),物理核心编号为0,2,4,6(跳过超线程对),执行:
# 绑定到核心 0,2,4,6,共 4 个物理核 taskset -c 0,2,4,6 ./stream_omp_float此时输出中Number of Threads counted应为4,且Best Rate MB/s值往往比全核运行更高(尤其在 DDR4 平台)。这是因为减少了内存请求仲裁开销,提升了 IMC 利用率。
为什么不用 numactl?
numactl --cpunodebind=0 --membind=0 ./stream_omp_float看似更规范,但它绑定的是 NUMA 节点,而非具体核心。在单路系统中效果等同taskset;但在双路系统中,若未显式指定--membind,OpenMP 可能跨节点分配内存页,导致非一致性访问(NUMA penalty),测出的反而是“伪带宽”。taskset更底层、更可控,是定位硬件瓶颈的第一步。
2.3 验证数组是否真正绕过缓存:用 perf 监控 LLC-misses
stream的设计哲学是“强制走内存”,但若STREAM_ARRAY_SIZE设得太小(如1e6),整个数组可能被 L3 缓存容纳,测的其实是缓存带宽,毫无意义。我们用perf工具验证是否真实触发 DRAM 访问:
# 在运行 stream 前,启动 perf 监控 LLC(Last Level Cache)缺失率 perf stat -e "cycles,instructions,cache-references,cache-misses,LLC-loads,LLC-load-misses" \ -x, ./stream_omp_float 2>&1 | grep -E "(LLC|cache|rate)"理想输出应包含:
LLC-loads,1234567890 LLC-load-misses,1220000000 LLC-load-misses rate: 98.9%判断标准:
LLC-load-misses rate > 95%才算合格。若只有70%,说明约 30% 数据命中 L3,数组太小,需增大STREAM_ARRAY_SIZE(如翻倍至400000000),重新编译再测。这是很多初学者翻车的第一坑——以为跑出了高带宽,实则测的是缓存。
3. 四大测试项背后的访存模式解密:Copy/Scale/Add/Triad 到底在考什么?
3.1 Copy:最基础的“读-写”流水线,暴露内存控制器读写平衡能力
Copy测试执行操作:a[i] = b[i]
→ 1 次读(b 数组) + 1 次写(a 数组) = 每元素 2 次内存事务
→ 理论带宽 =2 × STREAM_ARRAY_SIZE × sizeof(float) / min_time
这是最“干净”的测试:无数据依赖、无计算、纯搬移。它直接反映内存控制器的读写通道利用率。若Copy带宽显著低于其他项(如低 15% 以上),常见原因有:
- 主板 BIOS 中 “Memory Write Acceleration”(MWA)被禁用;
- DDR 颗粒质量不均,某条内存条写入时序不稳定;
- 使用了 ECC 内存且未开启 “ECC Scrubbing Disable”。
玄学经验:在部分 AMD EPYC 平台,
Copy带宽常比Triad低 3–5%,这是正常现象——因为 EPYC 的 IMC 对写合并(write combining)优化更强,而Copy无法利用该特性。
3.2 Scale:引入标量乘法,考验 CPU 向量化与内存预取协同
Scale执行:a[i] = q × b[i],其中q是常量(如3.0)
→ 1 次读(b)、1 次标量乘、1 次写(a)
→ 关键在于:乘法是否被编译器向量化?是否触发硬件预取器?
编译时-march=native至关重要。若 CPU 支持 AVX-512,gcc会生成vmulps zmm0, zmm1, zmm2指令,一次处理 16 个 float;若仅支持 SSE4.2,则用mulps xmm0, xmm1,一次 4 个。你可以用objdump -d ./stream_omp_float | grep vmulps查看是否生成了 AVX-512 指令。
血泪经验:某次在 Intel Xeon Platinum 8380 上,
Scale带宽比Copy低 12%,检查发现 BIOS 中 “Hardware Prefetcher” 被关闭。开启后Scale提升 9.3%,而Copy几乎不变——证明预取器对含计算的访存模式收益巨大。
3.3 Add:数据依赖链的起点,揭示内存延迟对吞吐的隐性压制
Add执行:a[i] = b[i] + c[i]
→ 2 次读(b,c)+ 1 次加法 + 1 次写(a)
→ 表面看是“2R1W”,但关键在地址局部性:b[i]和c[i]是否映射到同一内存通道?
现代内存控制器将地址哈希到不同通道(Channel A/B/C/D)。若b和c数组起始地址对齐方式相同(如都按 4KB 页对齐),它们可能被哈希到同一通道,造成单通道拥塞。stream源码中通过OFFSET宏(默认 0)可错开起始地址:
// 修改 stream_v5.10.c 中第 122 行附近: #define OFFSET 2000000 // 错开 200 万个 float 元素(8MB)重新编译后,Add带宽常提升 5–8%,这就是通道负载均衡的实证。
3.4 Triad:最贴近真实负载的“读-计算-写”闭环,暴露内存控制器调度瓶颈
Triad执行:a[i] = b[i] + q × c[i]
→ 2 次读(b,c)+ 1 次标量乘 + 1 次加法 + 1 次写(a)
→ 5 次内存/计算操作,但只产生 1 次写回
这是四个测试中最“重”的一项,也是最接近科学计算中y = alpha*x + beta*y(BLAS axpy)的模式。它的带宽往往最高,因为:
① 编译器可将q × c[i]和b[i] + ...合并为融合乘加(FMA)指令(若 CPU 支持);
② 内存控制器对“读多写少”的混合模式调度更高效。
注意:
Triad带宽不应超过Copy的 1.2 倍。若Triad达到Copy的 1.5 倍,大概率是数组未真正驻留内存(LLC miss rate < 90%),或编译器做了非法优化(如将整个循环提至寄存器)。务必用perf复验。
4. 避坑:stream 测试中 4 个真实踩过的坑与现场排查指南
4.1 现象:Number of Threads counted = 1,无论怎么设OMP_NUM_THREADS都无效
原因:系统缺少 OpenMP 运行时库,或gcc编译时未链接libgomp。某些精简版 Linux 发行版(如 Alpine)默认不带 OpenMP。
解决:
① 检查libgomp.so是否存在:find /usr -name "libgomp*" 2>/dev/null;
② 若不存在,安装对应包:Ubuntu/Debian 执行sudo apt install libgomp1,CentOS/RHEL 执行sudo yum install libgomp;
③ 重新编译,加-lgomp强制链接:gcc -O3 -march=native -fopenmp -lgomp ...。
4.2 现象:Triad带宽突降 40%,Copy正常,perf显示LLC-load-misses rate = 99.2%
原因:CPU 温度过高触发 thermal throttling,降频导致 FMA 指令吞吐下降,但内存控制器频率未降,故Copy影响小。
解决:
① 实时监控温度:watch -n1 "sensors | grep 'Package'";
② 若 Package temperature > 85°C,暂停测试,清理散热器灰尘或降低环境温度;
③ 在 BIOS 中关闭 “Intel Turbo Boost” 或 “AMD Precision Boost”,锁定基础频率再测,确认是否为温控问题。
4.3 现象:双路服务器上,taskset -c 0-7(第一颗 CPU)测得 120 GB/s,但taskset -c 8-15(第二颗 CPU)仅 75 GB/s
原因:第二颗 CPU 对应的内存插槽未插满,或插了不同规格内存(如一条 DDR5-4800,一条 DDR5-4400),导致整条通道降速至最低规格。
解决:
① 查看内存配置:sudo dmidecode -t memory | grep -E "(Size|Speed|Bank|Locator)";
② 确保每颗 CPU 对应的内存通道插槽成对、同品牌、同容量、同频率;
③ 进 BIOS,进入 “Advanced → Memory Configuration”,确认 “Memory Frequency” 对两颗 CPU 一致,且未出现 “Downclocked” 提示。
4.4 现象:stream输出中Min time和Max time相差 > 15%,波动剧烈
原因:系统后台进程干扰(如 logrotate、snapd、systemd-journald 刷盘)、或 CPU 频率动态调节(Intel SpeedStep / AMD Cool'n'Quiet)未关闭。
解决:
① 测试前关闭干扰服务:sudo systemctl stop snapd logrotate rsyslog;
② 锁定 CPU 频率:
# 查看当前策略 cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor # 切换为 performance 模式(需 root) echo "performance" | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor # 验证 grep "MHz" /proc/cpuinfo | head -3③ 重新运行stream,Min/Max time差值应 < 3%。
5. 进阶技巧:用 stream 定位 NUMA 不平衡与内存通道故障的三步法
5.1 第一步:分离测试——单 NUMA 节点内各通道独立打点
双路服务器通常有 2 个 NUMA 节点(Node 0 和 Node 1),每节点连接 2–4 条内存通道。要定位是“跨节点慢”还是“某通道坏”,必须拆开测。以 2 路 Intel Xeon 6348(28 核/56 线程,每路 4 通道)为例:
# 测 Node 0 的通道 A+B(核心 0-13) numactl --cpunodebind=0 --membind=0 taskset -c 0-13 ./stream_omp_float # 测 Node 0 的通道 C+D(核心 14-27) numactl --cpunodebind=0 --membind=0 taskset -c 14-27 ./stream_omp_float # 测 Node 1 的全部通道(核心 28-55) numactl --cpunodebind=1 --membind=1 taskset -c 28-55 ./stream_omp_float记录四组Triad带宽。若 Node 0 的 A+B 通道为 115 GB/s,C+D 通道仅 62 GB/s,则高度怀疑 C 或 D 通道内存条故障或插槽接触不良。
5.2 第二步:交叉验证——强制跨节点分配内存,复现 NUMA penalty
若怀疑跨节点访问慢,需人为制造跨节点内存分配:
# 在 Node 0 上运行,但内存分配在 Node 1 numactl --cpunodebind=0 --membind=1 taskset -c 0-7 ./stream_omp_float此时Triad带宽若跌至 40–50 GB/s(降幅 > 50%),说明:
① NUMA balancing 未生效(/proc/sys/kernel/numa_balancing= 0);
② 或 BIOS 中 “Node Interleaving” 被开启(应关闭,让 OS 管理 NUMA);
③ 或应用程序未做numactl --membind绑定,导致 page fault 时随机分配到远端节点。
5.3 第三步:故障隔离——用 memtest86+ 与 stream 结合做通道级诊断
stream能暴露带宽异常,但不能定位硬件故障。当某通道持续偏低(如稳定在 30 GB/s),需进一步验证:
| 步骤 | 操作 | 目的 |
|---|---|---|
| 1. 物理拔插 | 关机,拔下疑似故障通道的内存条(如 Node 0 的 Channel C),仅保留 A+B+D 三条 | 排除接触不良;若带宽恢复,说明该条故障 |
| 2. 替换测试 | 将该内存条换到另一台同型号服务器的 Channel A 插槽 | 确认是否内存条本体损坏(非主板插槽问题) |
| 3. Memtest86+ 全盘扫描 | 制作 U 盘启动盘,运行memtest86+ v6.20,选择 “Test All” 模式,至少跑 4 轮 | 检测内存颗粒坏块、ECC 纠错失败、地址线短路等底层故障 |
我的习惯:每次交付新服务器,我必做三件事——
① 用stream跑满所有通道,记录基线值;
② 用ipmitool sdr type memory检查 DIMM 温度与电压是否在规格内;
③ 用dmidecode -t memory导出内存拓扑图,标注每条内存的 Slot、Size、Speed、Manufacturer,存档备查。
这些动作耗时不到 20 分钟,但能在未来半年避免 80% 的“性能忽高忽低”类客诉。希望帮到你。
本文还有配套的精品资源,点击获取