
简介Linux 平台下评估内存带宽性能的 STREAM 基准测试工具源码包面向系统管理员、性能优化工程师以及需要进行硬件选型和底层调优的开发者。STREAM 由 John D. McCalpin 博士研发通过 Copy、Scale、Add、Triad 四类典型内存操作分别测试内存复制、比例运算、加法运算和组合计算场景下的连续带宽是业界广泛认可的内存性能衡量方法。资源包共包含 8 个文件压缩后仅 17KB主要由 Fortran 与 C 语言两套实现源码、Makefile 构建脚本、使用说明文档、许可证及版本历史记录组成便于在 Linux 环境中直接编译与运行。已有 2965 人学习使用。读者可获得完整可编译的基准测试套件通过调节数组规模、优化编译选项等参数复现不同负载下的带宽表现帮助定位内存控制器、缓存层次结构等硬件瓶颈为系统性能评估、硬件选型与配置调优提供可靠依据。同时可对比不同主机、内核参数或容器化部署环境下的测试数据提升服务器部署与调优的可观测性。1. stream 测的不是缓存容量内存带宽瓶颈为什么更难被发现服务器“越用越慢”看 CPU 占用不高、磁盘没写满业务就是起不来。这类机器我一般先跑一遍 Linux 内存性能测试工具 stream不是看内存够不够而是看 CPU 从内存里拿数到底有多快。内存带宽这个指标很隐蔽CPU 补丁、编译器版本、NUMA 节点跨了甚至 BIOS 里一个和节能相关的开关都会让带宽掉两三成而 top 和 free 却一点异常都看不出来。stream 解决的就是这个诉求用 Copy、Scale、Add、Triad 四种规则操作去压内存子系统量出持续带宽和对应的延迟表现判断瓶颈到底在访存通路还是在 CPU 算力。它适合三类人给服务器做性能评估的运维写高性能计算的开发以及刚拿到新机器想验证多通道内存是否生效的采购验收人员。测完得到一组数字后能不能信、和什么比才是真正花时间的部分。2. 先读懂 stream 在测什么带宽、翻倍率与访存模型2.1 内存性能不是看容量是看 CPU 能不能“吃饱”随手打开一个 Linux 终端看到的 free -g 只是容量它告诉你还能塞多少数据却不告诉你数据到 CPU 要等多久。真正卡住 CPU 的是内存带宽也就是单位时间内内存子系统能吐给 CPU 的字节数。现代服务器 CPU 的 L3 缓存已经做到 32MB 甚至 64MB写得很好的循环可以完全命中和它无关但数据规模一大缓存装不下就只能反复走内存控制器这时候带宽比频率更致命。带宽的上限是硬件决定的内存通道数、DDR 代数、频率、CPU 集成的内存控制器数这几个一起构成了理论峰值。但实际带宽还要看读写比例、访问是否连续、多核抢内存的并发程度这也就是 stream 这类基准工具存在的理由。CPU 主频高不代表带宽好我见过 3.0GHz 的至强跑不过 2.2GHz 的同代产品原因就是被测程序只跑单核内存控制器根本没被喂满或者编译器把循环优化成了寄存器操作压根没访问内存。2.2 看懂 stream 的四个内核Copy、Scale、Add、Triad 各自意味着什么stream 的源码由主程序和计时函数组成主程序里定义了四个数组 a、b、c然后反复执行四段循环分别叫 Copy、Scale、Add、Triad。它们不是四种测试模式而是四种最典型的访存模式覆盖了读密集、写密集和混合读写这三种情况。Copy 做的是c[i] a[i]对每个元素读一次、写一次操作字节数是数组中元素大小乘以二。Scale 是b[i] scalar * c[i]同样读一次写一次量上和 Copy 一样。Add 是c[i] a[i] b[i]读两个数组、写一个数组操作的字节数变成原来的三倍。Triad 是a[i] b[i] scalar * c[i]和 Add 一样读两个写一个但顺序和乘法结构略有不同软件优化时编译器能做的变换更多所以它最常被用来衡量内存带宽上限。这四个内核的价值在于某种内存控制器可能读带宽很好、写带宽很差只测 Copy 会误以为整体没问题。把四段循环的结果放在一起看才能判断瓶颈是出在读通路还是写通路。很多公开的 HPC 性能数据只报 Triad 数字那是因为 Triad 的数值最高最能彰显硬件峰值实际应用中反而要看 Add 和 Copy 的最低值那个才是你在常规业务里最常撞到的天花板。2.3 翻倍率是判断多通道是否生效的标尺翻倍率这个词不是 stream 官方输出的字段而是工程师拿单线程和满线程结果做除法得到的。计算方式很简单先记录单线程跑 Triad 的 Best Rate再记录全核跑同样操作的 Best Rate后者除以前者得到的就是扩展倍数。常见情况里普通双通道服务器能跑到 8 到 15 倍单通道或者跨 NUMA 节点访问时这个倍数掉到 3 以下很常见。这个数和带宽数值是互相校验的关系。如果你看到 Triad 带宽标称 40GB/s但翻倍率只有 2.5那就要怀疑带宽数字是缓存命中刷出来的虚高或者是 OpenMP 线程全部挤在同一个物理核上根本没有并发。反过来翻倍率看着有 12 倍但绝对带宽还不到理论峰值的六成那说明硬件多通道在但某个环节没接通多半是 BIOS 里隐通道配置有问题或者内存条没有按双通道规则插。拿翻倍率做第一层筛选能让你快速定位要不要深入看硬件配置。3. 下载编译到跑通Linux 上 stream 的最小操作路径3.1 编译前先确认 CPU 指令集与内存通道数stream 本身不挑指令集它有标准 C 的版本任何 Linux 发行版都能编译。但在编译之前花两分钟确认两件事能省掉后面大半的排查时间第一是 CPU 型号和内存通道数第二是末级缓存大小。这个信息后面确定数组大小时直接用到。# 查看 CPU 型号与编号 lscpu | grep -E Model name|Socket|Core|Thread|NUMA node # 查看内存条配置和通道数 sudo dmidecode -t memory | grep -E Size|Speed|Locator # 查看内存控制器的最大带宽能力 lspci | grep -i memorylscpu 输出里的 Socket 数量决定了有没有 NUMA 拓扑Thread 数决定了 OpenMP 线程数的上限。dmidecode 能看到每条内存条的大小和频率如果四条内存条分别落在一个 CPU 的不同通道上带宽表现会完全不一样。先记下这些信息后面跑出的数字才有参照物。预先知道物理通道数也很重要一台双路服务器如果只有单根内存条stream 无论怎么优化带宽数字都只有理论值的零头这是硬件配置问题不是工具没用好。3.2 用 gcc 编译 stream默认版和 OpenMP 版stream 源码发布时包含了 stream.c、mysecond.c 和几个 Makefile 文件。不需要安装额外依赖只需要系统里有 gcc 和 make。下载源码包解压后进入目录直接编译# 方式一默认编译串行执行 gcc -O3 stream.c mysecond.c -o stream # 方式二OpenMP 并行版推荐日常使用 gcc -O3 -fopenmp stream.c mysecond.c -o stream_omp # 方式三显式指定数组大小后面会重点讲 gcc -O3 -fopenmp -DSTREAM_ARRAY_SIZE33554432 stream.c mysecond.c -o stream_omp_big三种方式的区别在于是否需要多线程支持。默认版适用于单核虚拟机或者只想快速验证工具本身的场景多数服务器上直接被编译出 OpenMP 版本。-DSTREAM_ARRAY_SIZE这个宏是编译期覆盖默认数组大小的入口stream 源码里默认定义了 2000000 个 double 元素约为 16MB 的数组这个尺寸在现代 CPU 上基本只测到了 L3 的那一小块所以必须改大。如果系统里没有 gccAlmaLinux、Rocky Linux、Ubuntu 这些发行版都能用包管理器快速安装。在 Debian 系上执行 apt install gcc make在 Red Hat 系上执行 dnf install gcc makeWindows 上的 WSL 环境也可以照这个流程跑。有些发行版把 stream 直接打进仓库叫 stream 或者 stream-benchmark装完就能执行但这种情况还是建议手动编译一次因为你会同时掌握修改数组大小和线程数两个日后必用的参数。3.3 最小运行命令与输出识别编译完成后直接运行# 指定 4 个线程跑 OpenMP 版 export OMP_NUM_THREADS4 ./stream_omp # 输出关键信息预览 # ------------------------------------------------------------- # STREAM version # ------------------------------------------------------------- # This system uses 8 bytes per array element. # Array size 2000000, Offset 0 # Total memory required 48.0 MB. # Each test will run 10 times. # ------------------------------------------------------------- # Function Best Rate MB/s Avg time Min time Max time # Copy: 8561.2 0.007504 0.007473 0.007552 # Scale: 8590.6 0.007496 0.007465 0.007520 # Add: 8391.1 0.010282 0.010245 0.010310 # Triad: 8544.8 0.010134 0.010101 0.010188这个输出里第一段是配置回显数组大小 2000000 是默认值内存需求 48MB 对应三个 double 数组的总量每个测试跑 10 次。第二段才是结果Best Rate MB/s 是各内核的最高带宽Avg time、Min time、Max time 是三列时间统计时间越短越好。一组完整输出末尾还有 Solution Validates 的字样代表结果没有被编译器优化到“没干活”的状态这行字不能忽略。Min time 是分析时要用的主值因为它最接近没有任何调度噪声的状态。Copy 和 Scale 的操作字节数一样Rate 通常在 5% 以内如果 Copy 和 Scale 差多了优先怀疑写缓冲策略出了问题。首次跑通后先别急着改参数这个默认尺寸的结果不要当作最终结论它只能证明工具链没问题真要拿来评估机器尺寸必须按下一章的方法调整。4. 参数对照表数组大小、OpenMP 线程与 NUMA 绑核怎么搭4.1 从缓存容量反推 STREAM_ARRAY_SIZE避开缓存命中陷阱stream 默认的数组大小 2000000 个 double三个数组共约 48MB这个尺寸放在十年前够用放到现在只能和 L3 缓存玩捉迷藏。数据如果整体能被 CPU 的 L3 装下循环就会一直命中和内存无关跑出的带宽数字会高得离谱完全不能反映真实访存压力。让内存成为瓶颈的关键是数据体积要远超末级缓存常见做法是取 L3 容量的四到八倍。# 查看 L3 大小单位字节 getconf LEVEL3_CACHE_SIZE # 例如 L3 3355443232MB数组字节数取 8 倍 # STREAM_ARRAY_SIZE 32MB * 8 / 8B 33554432 个元素 gcc -O3 -fopenmp -DSTREAM_ARRAY_SIZE33554432 stream.c mysecond.c -o stream_omp这个计算的本质就是保证三个数组的总占用显著大于最后一级缓存。以 L3 32MB 的机器为例三个数组每个 32MB总 96MB按 8 倍取更稳每个数组 256MB总 768MB无论编译器怎么调度任何一段数据都不可能被完整留在缓存里。数组也不是越大越好超过单个 NUMA 节点内存的一半页表开销会明显拉高测试时长也成倍增加我一般控制在 L3 容量的八倍以内。改完数组大小后重看输出回显Total memory required 应该变成约 768MB 而不是 48MB这一步就是验证宏是否生效最直接的方法。如果你用的是-DSTREAM_ARRAY_SIZE但输出还停在 2000000多半是编译时用了缓存的旧对象文件先 make clean 再重编。4.2 OMP_NUM_THREADS 与 OMP_PROC_BIND 的配合OpenMP 版的 stream 默认用机器所有逻辑核在超线程开启的服务器上线程数顶到 64、96 甚至更多时带宽不会线性上升反而因为线程在物理核之间反复抢内存控制器而出现震动。常见做法是把线程数设成物理核心数而不是逻辑核心数比如一台 32 逻辑核、16 物理核的机器先试OMP_NUM_THREADS16。语法参数绑定同样不能省。运行时加上OMP_PROC_BINDclose让 OpenMP 把线程集中于靠近的核上避免它们被调度器打散到两个实体内存区域。绑核工具 numactl 则更彻底能同时限制使用的 CPU 节点和内存节点# 确认 NUMA 拓扑 lscpu | grep -A 5 NUMA node # 绑定到 node0 的 16 个物理核并从 node0 分配内存 export OMP_NUM_THREADS16 export OMP_PROC_BINDclose numactl --cpunodebind0 --membind0 ./stream_omp--cpunodebind0把进程锁在第一个 CPU 插槽的核上--membind0保证内存分配也只落在同一实体内存区域。跨 NUMA 节点的访问是 stream 跑出“越优化越慢”最常见的元凶线程在 node0、内存却全部压在 node1 上时每一次读都变成远端的 UPI 传输带宽可以掉 40% 甚至更多。双路机器上如果仅测单路能力这套组合是必加的否则结果里混入了跨路开销无法定位问题。4.3 一个可以直接抄的编译运行脚本参数组合通常在三次实验后才会稳定下来我把常用的一套直接写成脚本避免每次手敲漏项#!/bin/bash # stream_bench.sh - 依次编译并运行 stream输出带时间标记的结果 L3_BYTES$(getconf LEVEL3_CACHE_SIZE) ARRAY_ELEMS$((L3_BYTES * 8 / 8)) PHYSICAL_CORES$(lscpu | awk /^CPU\(s\):/{print $2}) # 编译 gcc -O3 -fopenmp -DSTREAM_ARRAY_SIZE$ARRAY_ELEMS \ stream.c mysecond.c -o stream_omp # 运行线程数取物理核数 export OMP_NUM_THREADS$PHYSICAL_CORES export OMP_PROC_BINDclose echo $(date) echo L3 Cache $L3_BYTES bytes, Array size $ARRAY_ELEMS echo Threads $PHYSICAL_CORES # 用 numactl 绑在 node0 时把下面这行注释切换 # numactl --cpunodebind0 --membind0 ./stream_omp ./stream_omp这个脚本里L3_BYTES * 8 / 8的乘除看似绕了一圈其实含义是“取 L3 容量的八倍做数组总字节数再除以每个元素的 8 字节”。写成分步算式而不是直接乘倍数是为了在日志里一眼看出推导路径。实际使用中如果机器有两个 NUMA 节点且内存分配不均数字还是偏低可以手动跑一次 numactl 版本对比差异超过 15% 就说明有跨节点访问需要在脚本里固定绑核策略。4.4 一些必须当场修正的编译选项stream 源码对编译器优化挺敏感-O2 和 -O3 的结果差距能到 10% 以上-O3 几乎是必选。-marchnative可以针对当前 CPU 指令集做优化能小幅提升数字但它会让二进制失去可移植性同目录的 stream 结果不能直接跨机器比对如果不是在同一批机器上做横向测试慎用。-ffast-math这类激进优化我从来不开它可能让编译器将浮点运算重排无论结果是更好还是更差测量口径都已经不干净了。还有一类情况是编译器直接把循环识别成死代码给优化没了输出里 Solution Validates 缺失就是典型信号。遇到这种只能改回更保守的优化等级或者检查源码里有没有被改坏。stream 的源码主体很短某行#ifndef STREAM_ARRAY_SIZE一旦弄坏整个评测逻辑就不可信了所以每次都重新解压一份干净源码再改参数是我逃避编译器魔改最省心的办法。5. stream 结果解读与避坑从五组数字中看出机器的真实情况5.1 带宽怎么算出来的Copy、Scale、Add、Triad 的字节口径stream 每个内核的带宽都是拿“操作的总字节数”除以测试时间得到的单位 MB/s。Copy 操作中每个元素被读一次、写一次共操作 2 个 double所以总字节数是 2 × 数组元素数 × 8 字节。Add 和 Triad 都读两个数组、写一个数组总字节数是 3 × 数组元素数 × 8 字节。用之前改的 N33554432 举例Copy 总字节数就是 2 × 33554432 × 8 536,870,912 字节约 512MBTriad 是 805,306,368 字节约 768MB拿这个数除以 Min time 得到 Rate。四个 Rate 值的大小关系也有规律Add 和 Triad 因为字节数多Rate 数值通常高于 Copy 和 Scale。如果一个结果里 Copy 反而最高那要么是写缓冲策略差异要么是数据被缓存命中了。做对比时只挑一个值没有意义要四个一起看特别是 Copy 和 Add 的差。现实的对照基准是内存理论带宽DDR4-2666 单通道的理论峰值是 21.3GB/s双通道是 42.6GB/s实际 stream 能跑到理论值的 75% 到 85% 就算正常。如果标称双通道 42.6GB/s 的机器 Triad 只有 15GB/s问题基本不在工具在硬件配置或绑核上。拿这个百分比值去衡量机器健康度比纠结某一个内核的快慢更稳。5.2 常见跑分翻车现场五个坑坑一数组太小带宽虚高到吓人现象Triad 跑到 20000MB/s 以上远超硬件理论值数组默认 2000000 没改。原因三个数组一共 48MBL3 缓存大一点的处理器直接全部命中测的是缓存带宽不是内存带宽。解决用 getconf LEVEL3_CACHE_SIZE 查缓存容量把数组改成 L3 的四到八倍重编重测。坑二单线程结果低得离谱就断言机器不行现象Copy 或 Triad 只有 3000MB/s 上下翻倍率接近 1。原因编译时忘了加-fopenmp或者运行时没设置 OMP_NUM_THREADSserial 版只跑了一个核。解决确认编译命令含 -fopenmp运行前 export OMP_NUM_THREADS物理核数。单线程数字不能代表整机带宽能力。坑三线程数拉满带宽反而比 8 线程还低现象一台 2 路 64 逻辑核的机器OMP_NUM_THREADS64 时 Triad 只有 25GB/s16 线程时反而有 50GB/s。原因超线程把两个线程挤在同一个物理核上争抢执行单元和缓存没有增加实际并行度线程又跨了 NUMA 节点内存访问全部变成远端。解决线程数设成物理核数默认开超线程的机器在 lscpu 里 Thread(s) per core 是 2用lscpu | grep Core(s)或者nproc --all的逻辑核心数除超线程倍数绑核加 OMP_PROC_BINDclose。坑四同一台机器两次跑数字波动超过 10%现象前一次 Triad 48GB/s后一次只有 41GB/s也没改任何配置。原因后台有监控进程、日志备份任务或另一波测试在抢内存带宽stream 对任何额外的访存都非常敏感。解决选系统空闲时跑重复 5 到 10 次取中位数而不是最大值。用taskset -c 2,6,10,14把测试线程钉在固定核上减少调度干扰。被干扰的测试结果别当作结论重跑一遍最稳妥。坑五重定向输出后Best Rate 变成了 0 或一个固定值现象./stream_omp result.log后打开文件只有 0 或者频频出现完全相同的时间。原因这是典型的计时函数适配问题stream 自带的 mysecond.c 在某些 Linux 发行版上拿不到高精度时钟串行版本会退化为粗糙的固定分辨率。解决重新编译时用系统自带的clock_gettime替换计时文件或者在源码目录里换用性能计数接口的版本。发行版里的 stream 包自带的 second.c 对多数 Linux 是兼容的但自己编的时候要注意这个问题。6. 把 stream 当基准工具用稳定复测与对照实验的闭环6.1 用硬件计数器交叉验证结论stream 的带宽是软件算出来的它把操作字节数除以耗时中间假设了每次访存都真实落到了内存。硬件事件计数器能验证这个假设Intel 平台的uncore_imc计数器可以读实际内存控制器流量AMD 平台对应有类似事件很多服务器自带 PMU 工具。交叉验证的做法很简单跑 stream 前后各读一次计数器值看差值是否接近 stream 报出的总字节数。# 常见做法用 perf 读取 uncore 内存带宽事件需要 root perf stat -e uncore_imc/data_read/ ./stream_omp如果硬件计数读出来的吞吐量和 stream 的 Best Rate 相差在两成以内说明软件数值可信。差值过大时优先怀疑数组没有真正访问到内存因为缓存命中的部分不会出现在内存控制器计数里。这个闭环对长期追踪服务器性能特别有用每一次 stream 跑分都可能存在无法量化的缓存噪声交叉验证把噪声的范围划出来了结论才经得住追问。6.2 前后对照实验的同口径原则做硬件改造、BIOS 升级、内核参数调整后大多数人会再跑一遍 stream 看效果但这中间的“对比”很容易翻车。同口径不只是相同命令它要求数组大小、编译器版本、是否 OpenMP、绑核策略完全一致任何一项变了结果差异里就混进了方法噪声。变量对照组实验组必须一致STREAM_ARRAY_SIZE3355443233554432一致OMP_NUM_THREADS1616一致OMP_PROC_BINDcloseclose一致gcc 版本与优化参数-O3 -fopenmp-O3 -fopenmp一致绑核策略numactl node0numactl node0一致被测硬件或系统参数默认 BIOS开启新特性唯一变量实验组和对照组之间只允许有一个变量这是基准测试的铁律。内存参数、NUMA 策略这些隐性因素很难一眼看出对照时最好保留上个月的完整运行日志我吃过亏更新了一个内核小版本后stream 数字掉了 8%第一反应查硬件最后发现是内核默认的透明大页策略变了数组页被拆成了小页TLB 未命中变多。如果当时没有记录内核参数这个结论很难定位。6.3 我常用的三条验证习惯第一结果取中位数而不是最高值。最高值反映的是最理想状态中位数接近稳定运行时的常态当作基线更有参考价值。第二日志里写全环境信息CPU 型号、内存条规格、编译参数、线程数这几项缺了任何一项三个月后回看结果就是灾难。第三如果只是临时复测某台有问题的机器我会同时跑 lscpu、free -h、dmesg 尾部把系统状态一条命令打成一个包排查时少走弯路。这个习惯帮我避免了很多次“只测到现象没测到原因”的无效结论。最后说个真实教训有次给一台 2 路服务器做采购验收第一次 stream 跑出非常漂亮的数字我直接在报告里写了“性能达标”。后来同事复测发现数字掉了一半查了一圈才确认我第一次是跑在单路节点上另一个 CPU 插槽根本没启用。那次之后我养成一个习惯——任何 stream 结果都要先确认 lscpu 里的 NUMA node 数量和线程绑核日志再下结论不然你就会在数字上建立一个完全不存在的“性能基线”以后所有对照全歪。希望做这步验证的你能绕过这个坑。提示stream 的数组大小、线程配置、绑核方法会直接影响结果可对比性正式记录时把编译选项和运行环境一起存下来才能让测试数据有长期参考价值。本文还有配套的精品资源点击获取