☰
Linux内存带宽测试工具STREAM实战:从编译到调参避坑指南
2026/10/10 3:56:07 网站建设 项目流程

简介:这一面向Linux系统优化与性能分析场景的资源包,提供STREAM内存带宽基准测试工具的完整源码与编译配置,适合运维工程师、开发者及硬件选型人员评估内存子系统表现,可快速建立内存性能基线。压缩包共8个文件,以C/Fortran源码、Makefile、README及历史说明文档为主,整体仅17KB,结构精简,可直接配合常见Linux发行版编译运行。目前已有2968人学习下载,印证了STREAM作为业界标准内存测试方法的参考价值。利用其中的stream.c等源码,可实测Copy、Scale、Add、Triad四类核心内存操作的最高带宽:Copy反映纯读写吞吐,Scale考察读取、计算与回写,Add和Triad进一步模拟并发读写下缓存与总线的处理能力。由此可定位内存控制器、缓存层级与系统总线瓶颈,为系统调优、硬件对比或科学计算环境选型提供量化依据,也可横向比较不同机器或纵向判断优化前后收益。

1. Linux内存性能测试工具stream:先搞清它测的是带宽而不是快慢

给服务器换完内存条,怎么确认带宽真的上去了?新到的开发板内存控制器参数对不对?这类问题用top、free都答不了,Linux内存性能测试工具stream是业内最常用来回答这个问题的工具。STREAM 不测内存延迟,也不跑分,它用四个循环让 CPU 持续读写三个大数组,测出内存子系统能稳定供应的带宽。一个反直觉的结论:CPU 算力翻倍容易,内存带宽翻倍很难,数据库、深度学习推理这类应用被拖住,常常不是 CPU 不够而是内存供不上。这篇文章适合做服务器选型、性能压测、评估嵌入式linux项目内存架构的开发者,从编译到调参再到避坑一次讲完。

2. STREAM的四个内核与指标口径:为什么不能只看一个数字

2.1 Copy、Scale、Add、Triad 四个内核分别测什么读写模式

STREAM 发布出来就是一个 stream.c 源文件,里面定义三个 double 数组 a、b、c,数组长度由 STREAM_ARRAY_SIZE 控制。四个内核做的事非常简单,就是带浮点乘法的数组循环。

四个内核的操作对应如下:

Copy: c[i] = a[i]; Scale: b[i] = scalar * c[i]; Add: c[i] = a[i] + b[i]; Triad: a[i] = b[i] + scalar * c[i];

每个内核的读写字节数不一样,这决定了它们模拟的是不同类型的负载:

内核操作读字节写字节总字节
Copyc = a8N8N16N
Scaleb = scalar * c8N8N16N
Addc = a + b16N8N24N
Triada = b + scalar * c16N8N24N

N 是数组元素个数,每个 double 占 8 字节。Copy 和 Scale 是读写 1:1,Add 和 Triad 是读多写少的 2:1。真实业务负载很少有纯读写 1:1 的,大多数查询和计算都是读多写少,所以如果只挑一个数字看,业内最常引用 Triad,因为它最接近科学计算里的 DAXPY 操作,也最能反映内存子系统在真实压力下的表现。四个内核全部跑一遍,能看出读写比例变化时带宽怎么跟着变,这比单个数字可靠得多。

2.2 有效带宽按什么口径算:为什么取 Best Rate 而不是平均值

STREAM 输出里每条结果末尾有 Rate 列,单位是 MB/s。计算口径很简单:该内核的总字节数除以内部计时器测出的运行时间。每个内核默认重复 10 次,也就是 NTIMES=10,最后只报 Best Rate,即用最小运行时间算出来的那个带宽。

第一次接触的人常觉得这数字虚高,是在挑最好成绩。实际上 STREAM 测的不是业务平均性能,而是内存子系统在持续满负荷下能达到的稳定峰值吞吐。取最小时间是为了过滤操作系统调度、中断、后台任务带来的偶发噪声,保证不同机器之间有可比性。这也是为什么 STREAM 报告性能时只看 Best Rate 的原因。

需要强调一点:单看 Copy 一个数字没有意义。Copy 带宽高但 Triad 低,说明读多写少场景可能有问题;四个数值接近,说明内存子系统表现均衡。如果你拿到一份测试报告只贴一个 Copy 数值,这份报告基本可以判断为外行做的。

2.3 数组为什么要远大于 CPU 缓存

这是新手最容易翻车的一个点。假设你把 STREAM_ARRAY_SIZE 设成几万,数组只有几百 KB,而处理器 L3 缓存有几十 MB,循环一跑,整个数组都留在缓存里,测出来的是几百 GB/s 甚至上 TB/s 的缓存带宽,不是内存带宽。这个数字再好看,也不能说明内存条行不行。

STREAM 官方建议数组大小至少是最大缓存容量的 4 倍。常见做法是把 STREAM_ARRAY_SIZE 设成 2000 万到 4000 万,每个数组约 160MB 到 320MB,三个数组合计约 480MB 到 960MB,远远超过任何处理器的 L3 缓存。这背后其实是 Linux 底层原理里的缓存层级与一致性模型,理解了数组规模设计,等于顺带复习了一遍 CPU 缓存结构。跑正式测试之前看一眼free -h,确认内存够用,避免测试中途触发 swap。

3. 编译与最小跑通:从 stream.c 到能出结果

3.1 下载 stream.c 并用最简命令编译

STREAM 不需要安装依赖库,整个工具就是一个 stream.c 文件。从 STREAM 官网把标准版本下载到本机,先做最简编译验证环境能跑:

# 最简编译,不启用OpenMP,串行版本 gcc -O2 stream.c -o stream # 运行,看到输出即表示环境正常 ./stream

这条命令编译出来的是串行版本,跑完会输出 Copy、Scale、Add、Triad 四行结果,以及运行时的线程数。如果提示找不到 gcc,先安装编译工具链。整个过程只需要 gcc、numactl、lscpu 这几个 Linux 常用命令,不需要额外装任何库。第一次跑通的意义是确认源码没问题、工具链没问题,再往下调参数才有意义。

3.2 编译参数怎么选:为什么官方建议用 O2 而不是更激进

串行版本验证通过后,正式测试一般用 OpenMP 多线程版本。编译参数组合如下:

编译命令适用场景说明
gcc -O2 stream.c -o stream本机快速验证串行版本,结果偏低
gcc -O2 -fopenmp stream.c -o stream_omp多线程标准测试最常用的正式测试版本
gcc -O2 -fopenmp -DSTREAM_ARRAY_SIZE=20000000 stream.c -o stream调整数组规模后测试显式覆盖默认数组大小
gcc -O2 -fopenmp -static stream.c -o stream_static拷贝到其他机器运行静态链接,避免缺 libgomp
# 本机确认硬件极限时,可以用更积极的优化 gcc -O3 -march=native -fopenmp stream.c -o stream_native

-O2是官方建议的基础优化级别。不要一上来就-O3 -march=native,因为优化器可能把循环结构改写,导致你测的不是内存子系统,而是编译器优化后的代码。跨机器对比时,用同一份-O2编译出的二进制拷贝到各台机器跑,保证二进制一致,结果才有可比性。本机单独确认硬件能力时,可以用-O3 -march=native。加-fopenmp是启用 OpenMP 多线程版本,运行端用OMP_NUM_THREADS控制线程数。静态链接用于把二进制拷到没有装 OpenMP 运行库的机器上跑的场景。

提示:不要加-ffast-math这类浮点激进优化,会改变浮点计算语义,测出来的带宽不代表真实应用表现。

3.3 跑通前先看清 NUMA 拓扑:绑核是第一步

多路服务器上,CPU 访问本地内存和远端内存的速度不一样。如果测试进程在 NUMA 节点间漂移,带宽结果会偏低且不稳定。正式测试前先看拓扑:

# 查看CPU和内存节点分布 lscpu numactl --hardware

如果numactl --hardware显示有 node0 和 node1 两个节点,说明是双路 NUMA 架构。绑定到单一节点跑:

# 绑定到node 0的CPU,且内存从node 0分配 numactl --cpunodebind=0 --membind=0 ./stream

不加绑定时,操作系统调度器可能把进程在节点之间迁移,内存页也可能跨节点分配,结果方差很大。对比测试一定要加同样的绑核参数。绑完核之后,这个数字才代表某个节点真实的本地内存带宽。这一步不做,后面所有的排查都可能白做。

4. 调参与结果解读:数组规模、线程数与带宽换算

4.1 STREAM_ARRAY_SIZE 怎么设:缓存规避与内存占用

不要依赖源码里的默认数组大小,正式测试要显式覆盖:

# 每个数组2000万个double,约160MB;三个数组合计约480MB,远超L3 gcc -O2 -fopenmp -DSTREAM_ARRAY_SIZE=20000000 stream.c -o stream # 正式测试建议把重复次数从默认的10提高到20,过滤偶发噪声 gcc -O2 -fopenmp -DSTREAM_ARRAY_SIZE=20000000 -DNTIMES=20 stream.c -o stream

涉及的运行参数汇总如下:

参数建议值说明
STREAM_ARRAY_SIZE最大缓存容量的4倍以上,推荐2000万~4000万是元素个数,不是字节数
NTIMES10~20每个内核重复次数,取Best Rate
OMP_NUM_THREADS物理核数运行时环境变量,控制并行线程数
OMP_PROC_BINDtrue绑定线程,防止核间漂移

数组大小和物理内存要匹配。总内存只有 4GB 时,三个数组 480MB 没问题;如果设成 1 亿,三个数组共 2.4GB,加上系统页缓存很容易吃紧,触发 swap 后结果完全失真。数组大小不是越大越好,够规避缓存就行,太大了反而引入内存不足的风险。

4.2 OpenMP 线程数与亲和性:为什么开满逻辑核反而容易虚高

STREAM 的并行版本靠 OpenMP 把数组循环拆给多个线程。运行时通过环境变量控制:

# 指定8个线程 OMP_NUM_THREADS=8 ./stream # 绑定线程到物理核,避免线程在核间漂移 OMP_NUM_THREADS=8 OMP_PROC_BIND=true ./stream

内存带宽打满需要同时有足够多的在途访存请求。线程太少,并发请求数不够,带宽上不去;线程太多,比如开满超线程的逻辑核,逻辑核之间争抢访存单元,收益边际递减,还引入调度抖动。常见做法是设为物理核数,查看lscpu输出里的 Core(s) 和 Thread(s) per core 可以确认物理核数。OMP_PROC_BIND=true让线程固定下来,防止操作系统把一个线程在不同核之间搬来搬去。绑定之后,每个线程访问的本地内存位置更稳定,结果也更接近硬件真实水平。

4.3 结果表怎么读:四行联看,反推硬件规格

跑完的典型示意输出如下(数值仅用于说明):

Function Rate (MB/s) Copy 38120.6 Scale 38231.4 Add 42156.2 Triad 42683.9

四行数值接近,Add 和 Triad 比 Copy 略高,属于正常现象,因为读多写少时访存总线切换开销更小。如果四行差距很大,回到上一章排查编译和绑核。用 Triad 可以反推硬件规格:Triad 约 42.7GB/s,而 DDR4-3200 双通道理论带宽是 3200MT/s 乘 8 字节乘 2 通道,约 51.2GB/s,实测是理论值的 83% 左右,说明内存基本跑满了。如果实测只有 25GB/s 左右,基本可以断定内存跑在单通道;如果只有 30GB/s 且内存条插了两根,优先怀疑频率没有跑在标称值。这些判断在 Linux 运维故障案例里非常常见,先看数字再查插槽,比盲目换硬件高效得多。

5. STREAM常见问题排查:五个典型翻车现场

5.1 结果远低于理论带宽:先查线程数、数组大小和 NUMA

现象:硬件标称双通道 51.2GB/s,实测不到 20GB/s。

原因:最常见的有三种。第一,还在跑默认的串行版本,单线程远不能打满多通道内存;第二,数组太小,结果命中 L3 缓存,数字虚高或者不稳定;第三,进程被调度到远端 NUMA 节点,访问远端内存导致带宽大幅下降。

解决:按顺序排查。先确认编译加了-fopenmp且运行设了OMP_NUM_THREADS,再看STREAM_ARRAY_SIZE是否足够大,最后加numactl --cpunodebind=0 --membind=0绑核重跑。我遇到过低带宽的问题,最后发现是线程数设成了 1,改回物理核数后带宽直接翻倍。

5.2 Copy 和 Triad 的带宽关系异常:优化器与总线切换的博弈

现象:Copy 只读写 16N 字节,Triad 读写 24N 字节,理论上 Copy 应该更快,但有时 Triad 比 Copy 还快。

原因:这里有两种情况。正常情况是,内存控制器对读写总线切换有开销,Copy 是读写 1:1,总线切换频繁,反而不如读多写少的 Triad 效率高,这是硬件特性不是 bug。异常情况是,如果 Copy 明显快过 Triad 一大截,要怀疑编译器把 Copy 循环优化掉了,比如某些激进优化选项把c[i]=a[i]整段搬成缓存操作,测出来的不是内存访问。

解决:统一用-O2编译做对比,保持二进制一致。想验证是否被优化,用objdump -d stream查看 Copy 循环里是否还有真正的 load/store 指令。优化问题我踩过不止一次,后来学乖了,对比测试永远只用-O2。

5.3 跑了半天还是单线程:OpenMP 没生效

现象:运行输出里 Threads counted 显示为 1,Rate 和串行版本一样。

原因:编译时没加-fopenmp;或者编译加了,但运行环境缺少 libgomp 运行库;或者环境变量里设置了OMP_NUM_THREADS=1,OpenMP 运行时严格按这个值启动线程。

解决:重新编译时加-fopenmp,运行前用ldd stream检查 libgomp 是否链接到,找不到就改用-static静态编译。运行时显式设置OMP_NUM_THREADS=物理核数,不要依赖默认值。输出里 Threads counted 这行每次都要看一眼,这是最快的判断依据。

5.4 同机多次跑结果波动:频率、散热与后台负载

现象:同一台机器,间隔十分钟跑两次 Triad,结果差了 10% 以上。

原因:CPU 频率动态变化,睿频和功耗墙导致频率不稳;散热不良触发降频;后台监控进程抢占内存带宽;同一个进程被调度到不同 NUMA 节点。

解决:能锁频就锁频,不能锁就固定在高性能模式;绑核并设置OMP_PROC_BIND=true;关掉后台的监控采集和日志任务;正式记录时连续跑三遍,只取 Best Rate。对比两台机器时,必须保证编译参数、线程数、数组大小、绑核方式完全一致,否则数字没有可比性。这是做评测的基本纪律。

5.5 大数组编译或运行出错:下标溢出与内存不足

现象:把STREAM_ARRAY_SIZE改得很大,编译直接报错,或者运行到一半段错误。

原因:stream.c 传统版本数组下标用 int,当STREAM_ARRAY_SIZE超过约 21 亿时下标溢出;数组是静态分配的,三个数组总大小超过物理内存时会触发 OOM 或 swap;32 位环境下静态数据段有 2GB 限制。

解决:保持数组总大小不超过物理内存的一半。确实需要超大数组时,用新版本 stream.c 并加-DSTREAM_MALLOC让它改用 malloc 动态分配,同时确认编译器是 64 位。实际业务里很少需要每数组大于 2 亿元素,遇到报错先回头检查内存总量,别硬上大数组。

6. 进阶用法:用 STREAM 验证内存子系统配置是否合理

6.1 逐 NUMA 节点对比:本地带宽与远端惩罚

双路服务器上分别跑三组测试:

# node 0 上跑一次 numactl --cpunodebind=0 --membind=0 ./stream # node 1 上跑一次 numactl --cpunodebind=1 --membind=1 ./stream # 故意跨节点:CPU 在 node 0,内存在 node 1 numactl --cpunodebind=0 --membind=1 ./stream

比较三组的 Triad 值。前两个应该接近,第三个如果明显低,说明跨节点访问有惩罚,属正常现象;如果前两个差距也很大,检查该节点的内存条是否插满或插对槽位。这个套路在收二手服务器和验收新机器时非常实用,能很快暴露内存插法不对或者某一节点内存通道损坏的问题。

6.2 理论带宽怎么算:DDR4 和 DDR5 的快速对照

内存配置理论带宽备注
DDR4-2666 双通道42.6 GB/s2666MT/s × 8字节 × 2通道
DDR4-3200 双通道51.2 GB/s3200MT/s × 8字节 × 2通道
DDR5-4800 双通道76.8 GB/s4800MT/s × 8字节 × 2通道
DDR5-5600 双通道89.6 GB/s5600MT/s × 8字节 × 2通道

计算公式就是内存速率乘 8 字节乘通道数。STREAM 实测通常是理论值的 70% 到 85%,达到这个区间说明硬件工作正常。如果低于 60%,优先怀疑内存没跑在标称频率,比如 BIOS 里没开 XMP/EXPO,或者内存条插在了同一通道导致单通道运行。这个公式也是很多 Linux 面试题里喜欢考的点,记住一次能省很多排查时间。

6.3 用 perf 确认访存真的落在内存层级

# 看LLC miss,确认大量访问落到内存 perf stat -e LLC-load-misses,LLC-loads ./stream

LLC-load-misses 除以 LLC-loads 的比例高,说明大量访问确实落在内存层级,STREAM 结果可信;如果 miss 比例很低,说明数组还在缓存里,回看数组大小设置。我每次换平台、换内存、调 BIOS 后,都用同一套编译参数和数组大小跑一遍 Triad,把数字记下来当 baseline,后面应用优化遇到瓶颈就回来对照。这个习惯帮我排除过好几次应用变慢其实是内存降频的问题。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询