ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux内存带宽测试:STREAM跑分从编译到避坑全指南

Linux内存带宽测试:STREAM跑分从编译到避坑全指南 简介面向Linux系统优化与性能分析人员这份资源提供了STREAM内存基准测试工具的可直接编译源码专门用于评估计算机内存连续带宽解决内存性能难以量化对比的问题适用于系统管理员、开发者和硬件测试工程师。压缩包共包含八个文件涵盖C语言与Fortran源码、Makefile构建脚本、README说明文档以及历史版本记录整体体积仅十七KB结构简明清晰。已有2960人学习下载工具运行时通过执行复制、缩放、相加与三元运算四类操作输出对应的内存带宽和耗时数据帮助定位内存子系统瓶颈。还可调整数组大小与并行度参数深入考察内存控制器、总线与缓存层次结构的表现为高性能计算、大数据处理等内存敏感场景的优化提供客观依据亦可用于硬件选型与性能对比。1. Linux内存性能测试工具STREAM实测带宽才是调优的起点Linux服务器内存性能测试我一般先从STREAM跑分开始而不是直接翻内核参数。STREAM是一套用C语言写成的内存带宽测试工具通过Copy、Scale、Add、Triad四类循环操作测出系统在持续读写负载下的真实带宽和厂商标称的理论值往往差一大截。它能帮你判断内存通道是否完整、频率是否达标、NUMA拓扑有没有拖后腿也能用于服务器选型时横向对比。适合运维工程师、性能测试人员和做硬件评估的人。跑分本身不复杂但编译参数和运行方式选不对结果会骗人这篇笔记把从编译到避坑的完整过程拆一遍。2. 先搞懂STREAM在测什么四个内存操作模式与带宽模型2.1 四个测试模式Copy、Scale、Add、Triad的读写差异STREAM的源码只有一个stream.c核心跑了四个循环。用最直白的伪代码展示就是这样// copy: 读数组 a写数组 b for (j 0; j N; j) { b[j] a[j]; } // scale: 读数组 a乘常量后写数组 b for (j 0; j N; j) { b[j] scalar * a[j]; } // add: 读数组 a 和 b相加后写数组 c for (j 0; j N; j) { c[j] a[j] b[j]; } // triad: 读数组 a 和 b乘加后写数组 c for (j 0; j N; j) { c[j] a[j] scalar * b[j]; }四段代码虽然都只是循环体里几个赋值语句访存特征却不一样。我整理成一张表方便对照模式读操作写操作计算操作访存特征Copy11无读写均衡压力最纯粹Scale111次乘法读写均衡带轻微计算Add211次加法读多写少读带宽压力更大Triad211次乘加读多写少最接近科学计算为什么不能只跑一个Copy就完事我自己的理解是真实程序里的内存访问几乎不会只有纯复制场景。数据库的顺序扫描接近Copy列存计算接近Add科学计算里的矩阵更新接近Triad。不同模式对内存控制器的调度压力不一样Copy快不代表Add也快把四种模式都跑一遍才能看出内存子系统的偏向性。比如某台机器Copy成绩正常而Add明显偏低大概率是硬件prefetch对多读流支持得不好也能侧面反映L2/L3预取器的工作状态。这里补一个现象你拿到的STREAM输出里Copy经常是最低的那一项Triad通常最高。这不是机器坏了。一种常见解释是部分处理器架构对写操作采用写分配策略一次写会先在Cache里占用一行带出一次隐含读让实际总线上的读流量高于统计值而Triad里读操作更密集硬件prefetch更容易满负荷工作测出来的带宽反而更高。所以厂商公布内存带宽时引用的往往就是Triad值你跟厂商数据对比前先确认对方用的是哪个模式。2.2 数组大小与迭代次数为什么要设成Cache的4倍以上STREAM设计目标是测“持续内存带宽”也就是数据流突破各级Cache之后真正压到内存控制器和DRAM上的吞吐。如果数组太小数据全被L2/L3装下跑出来的是Cache带宽而不是内存带宽数值可能膨胀到几十倍毫无参考意义。原始包默认的STREAM_ARRAY_SIZE是10000000也就是1千万个double元素单个数组80MB四个数组加起来占320MB。这个默认值在十几年前够用但现在的服务器L3动不动几十MBEPYC这类大Cache平台默认值甚至可能落在L3里。John McCalpin最初的建议是单个数组大小至少是系统最大Cache的4倍。怎么判断数据有没有真正走出Cache有个土办法把数组大小翻一倍重跑一轮如果跑分明显上升说明刚才的数据没完全突破Cache继续加大如果跑分基本不变甚至略降说明已经在访存了当前值可用。迭代次数由NTIMES宏控制默认10次。它的作用是让同一批数据重复跑取最小耗时来抵消系统调度的抖动。次数太少计时误差大太多会让内存长时间满负载反而触发CPU降频。我在生产机器上一般设20到30次让单次运行时间控制在20秒到1分钟既稳又不至于热过头。数组大小还要考虑物理内存上限。四个数组加起来的字节数最好控制在物理内存的1/4以内否则操作系统会不断做页面回收测试过程里夹杂swap的代价跑分被严重拖低。64GB内存的机器四个数组加起来别超过16GB。2.3 为什么STREAM适合做跨平台对比而不是微基准STREAM代码是纯标准C不依赖平台私有API也没内联汇编同一份stream.c在x86、ARM、RISC-V上都能编译。跨平台可比性是它被广泛接受的核心原因。再加上它测的是“编译器生成指令 内存控制器 DRAM”的综合表现和你业务程序实际跑出来的访存行为更接近不像某些微基准会用特殊指令绕开瓶颈去压极限。这也带来一个必须接受的事实STREAM结果里包含了编译器的贡献。同一个CPUgcc 9和gcc 12编译出的二进制跑分可能差5%以上因为优化器对循环的向量化方式、prefetch插入位置、循环展开次数都会变。所以横向对比时编译器版本和优化参数不一致的结果不要拿来硬比。这一点在避坑章节我会再详细展开。3. 从源码到跑分STREAM编译参数与完整执行流程3.1 获取源码与基础编译STREAM官方提供的是单个stream.c源文件不依赖第三方库下载和编译都很轻量mkdir -p ~/stream cd ~/stream wget https://www.cs.virginia.edu/stream/FTP/Code/stream.c gcc -O3 -o stream stream.c ./stream先解释这段命令做的事wget把官方维护的stream.c拉到当前目录gcc直接编出可执行文件./stream跑默认配置。默认配置下STREAM_ARRAY_SIZE是1000万单个数组80MBNTIMES是10单线程运行。这个基础编译只适合做“程序能跑”的冒烟验证别拿它当结论。提示数组大小和迭代次数都可以通过编译期的-D宏覆盖不需要手动改源码后面会用到。3.2 按CPU架构调整编译优化参数要测出接近真实内存带宽的值编译参数必须认真调。我在Linux物理机上最常用的一组命令是gcc -O3 -marchnative -fopenmp \ -DSTREAM_ARRAY_SIZE200000000 -DNTIMES20 \ -o stream stream.c逐一说明参数含义。-O3是编译优化等级启用循环展开、自动向量化和软件流水不加O3的STREAM基本等于没测。-marchnative让编译器按当前CPU支持的指令集生成代码x86上会启用AVX2支持AVX-512的平台会启用AVX-512。-fopenmp链接OpenMP运行时STREAM源码里带有#ifdef _OPENMP并行分支打开后才能多线程跑。-DSTREAM_ARRAY_SIZE200000000把数组元素个数改成2亿每个double占8字节单个数组1.6GB四个数组共6.4GB。-DNTIMES20表示每个模式重复跑20次取最小耗时。Intel编译器也一样常见写法是这样icx -O3 -xCORE-AVX2 -qopenmp \ -DSTREAM_ARRAY_SIZE200000000 -DNTIMES20 \ -o stream stream.c-xCORE-AVX2直接指定指令集比-marchnative的自动检测更可控。Intel在老代码上有时能压出比gcc更高一点的分数因为对循环的展开更激进。AMD平台我基本用gcc -marchnative省事也稳定。还有一种玄学做法是只开-O3 -mtunenative不引入新指令集在某些降频敏感的CPU上反而成绩更高感兴趣的可以对比试一轮。3.3 执行STREAM与输出格式解读单线程冒烟这样跑OMP_NUM_THREADS1 ./stream整机多线程压测export OMP_NUM_THREADS16 ./stream线程数不要盲目拉到满。先确认CPU拓扑再定线程数常见做法lscpu | awk -F: /^CPU\(s\):/{print $2}跑完的典型输出长这样------------------------------------------------------------- Function Best Rate MB/s Avg time Min time Max time Copy: 112345.6 0.1021 0.0956 0.1032 Scale: 110234.5 0.1043 0.0974 0.1050 Add: 123456.7 0.1399 0.1298 0.1412 Triad: 124567.8 0.1408 0.1301 0.1425 -------------------------------------------------------------这里单位的细节要留意Best Rate的MB/s是10^6字节/秒不是MiB/s1024^2。Copy的112345.6 MB/s表示每秒搬运约112.3GB的数据这个数字里同时包含了读和写的流量。厂商公布带宽时如果用的是GB/s而没有说明基数对比时会差出大约2.4%这是个容易忽略的换算坑。输出里最该看的是Best Rate它是多次运行里最快的一次对应Min time。Avg time和Max time反映系统抖动。如果Max time比Min time大出20%以上说明测试过程中有后台负载干扰或CPU降频这次跑分需要重测。多线程线程数的影响也值得记一笔单线程跑反映单个核心能压出的访存带宽多线程跑反映整机内存控制器上限。线程数从1逐步加到满核带宽会先涨后平持平的那条线就是平台真实带宽上限。如果线程数翻倍带宽还在明显涨说明之前没喂饱内存控制器。4. 跑分前的关键配置数组大小、NUMA绑定与带宽验证4.1 数组大小怎么改才算合理数组大小决定测试是否真正压到内存而不是Cache。先说结论上限是四个数组总字节数不超过物理内存的1/4避免swap干扰下限是单个数组大小超过L3 Cache容量的4倍。结合常见机器配置给个参考表机器配置L3 Cache参考值STREAM_ARRAY_SIZE建议四个数组占内存消费级台式机16GB内存32MB50,000,0001.6GB双路服务器64GB内存64MB200,000,0006.4GB大Cache平台256GB内存以lscpu实测为准建议按L3的4倍换算不超过总内存1/4判断当前数组大小是否合适最直接的办法是翻倍重跑把STREAM_ARRAY_SIZE改成原来的2倍重新编译后再跑一轮。如果Best Rate涨幅超过5%说明之前的数据没完全走出Cache需要继续加大如果变化在2%以内说明带宽已到平台上限这个数组大小就够用。有些场景下数组大小改太大反而坏事。四数组总字节数超过物理内存一半后页面回收压力陡增测试时间也变长跑分里混进不少非内存带宽的时间损耗。我一般宁愿多跑几轮小数组也不让单机测试里出现swap痕迹。4.2 NUMA拓扑确认与绑定多路服务器上NUMA是STREAM最大的变量不处理直接就跑可能得到一份废数据。先确认拓扑lscpu | grep -E NUMA node|CPU\(s\) numactl --hardware输出里能看到available: 2 nodes (0-1)node0有16个CPU和64GB内存node1同规格。在这种机器上直接./stream线程可能被调度到两个节点内存页面也会在两个节点的本地内存里交叉分配跨节点访问的延迟和带宽消耗会明显干扰结果。单节点评估按这样绑定export OMP_NUM_THREADS16 numactl --cpunodebind0 --membind0 ./stream--cpunodebind0把进程限定在node0的CPU核心上--membind0把内存分配限制在node0的本地内存。两个参数同时给才能保证线程和内存页面都在同一个节点内。如果目标是测整机所有内存控制器同时工作的聚合带宽用interleave策略numactl --interleaveall ./streaminterleave模式会把内存页依次轮转分配到所有节点避免热点内存集中在某个节点导致的带宽倾斜。多路服务器做整机评估时interleave跑出来的值才有意义单节点评估还是用membind。OpenMP线程的分布方式也要管否则可能两个线程挤在同一物理核上抢执行单元。配合使用这两个环境变量export OMP_PLACEScores export OMP_PROC_BINDcloseOMP_PLACEScores让线程绑定到整核OMP_PROC_BINDclose让线程按紧凑方式依次占用相邻核心。检查CPU是否开了超线程用lscpu看Thread(s) per core如果为2这两个变量能避免超线程带来的额外干扰。4.3 结果计算与理论峰值带宽验证跑完别只记分数先用理论公式反推一下合理性。DDR4理论带宽公式理论带宽(MB/s) 内存频率(MT/s) × 单通道位宽(8字节) × 通道数DDR4-3200搭配8通道的计算3200 × 8 × 8 204800 MB/s ≈ 200 GB/s常见平台的参考值内存规格通道数理论峰值带宽DDR4-2666485.3 GB/sDDR4-29338187.7 GB/sDDR4-32008204.8 GB/sDDR5-48008307.2 GB/s注意DDR5的架构和DDR4不同单条模组内部通道逻辑有变化单纯用上述公式算出来的值通常偏乐观DDR5平台建议直接拿STREAM实测值跟同平台已知成绩做对比。确认当前内存实际频率用dmidecodesudo dmidecode -t memory | grep -E Type:|Speed:|Configured Memory Speed:|Size:输出里Speed是内存条标称最高速度Configured Memory Speed是当前实际运行速度两者不一致就是降频的最好证据。比如8根DDR4-3200跑在2666STREAM分数会比标称低一截这类问题单独看lscpu根本不暴露必须靠dmidecode加STREAM一起定位。最后给一个效率判断清单指标合理区间建议动作Triad带宽/理论峰值DDR4通常65%~85%低于60%查通道数和频率Copy带宽/理论峰值通常略低于Triad注意写分配影响多线程带宽/单线程带宽应接近内存控制器上限若翻倍线程分数仍涨继续加线程5. 避坑指南STREAM跑分常见的五个翻车现场与排查方法STREAM的坑大多不在工具本身而在编译选项和运行环境。下面五条都是我在不同机器上实测踩过的按现象、原因、解决的顺序写。5.1 编译器把循环优化没了跑分虚高到离谱现象Copy跑出300GB/s甚至更高明显超过内存理论峰值或者四个模式分数基本相同像是从Cache里读出来的。原因编译器在高优化级别下分析出循环写入的数据从未被使用直接把循环体删除这就是dead code elimination。STREAM源码里有一段用于校验和值的机制就是为了防这种优化。改动过源码或者加了-lto这类链接时优化选项就很容易踩上。解决编译命令保持gcc -O3 -marchnative不加-flto。运行后留意输出末尾的校验信息只要校验通过就说明循环没有被删。如果校验被绕过了结果一律作废。还有一种类似现象来自数组太小数据全命中L2Copy成绩可能超过理论峰值三倍但那种情况改大数组大小就能解决两个原因要对号入座。5.2 双路机跨NUMA节点访存分数反而低于单路现象双路机器默认方式跑STREAM成绩比同配置单路机还低或者同一台机器两次跑分波动超过10%。原因进程被任意调度到node0或node1内存页面由伙伴系统从某个节点分配两边没有对齐。节点间的UPI链路带宽有限跨节点流量一大整体带宽就被链路卡住。解决先numactl --hardware摸清拓扑单节点对比用--cpunodebind0 --membind0整机聚合测试用--interleaveall并配合OMP_PLACEScores控制线程分布。虚机里没装numactl的话用taskset也能绑CPU但物理机上还是numactl最省事。5.3 CPU降频和超线程让成绩波动不止现象连续三轮STREAMTriad最好值和最差值相差3%以上Max time是Min time的两倍。原因STREAM的向量化循环负载重AVX-512指令会让CPU频率自动下探散热不够再叠加降频带宽数据自然漂移。解决跑分前让机器空转1分钟预热跑分过程记录频率turbostat --show freq -i 2 # 或者轻量一点 watch -n 2 grep MHz /proc/cpuinfo如果看到频率阶梯式下探等系统凉了再重测。比较严谨的做法是BIOS里固定P-state服务商允许的前提下做同一组对比否则每次跑分都记录温度和频率区间标注在结果里。超线程方面把OMP_NUM_THREADS设成物理核心数往往比直接设满逻辑线程数更稳lscpu | awk -F: /^CPU\(s\):/{cores$2} /^Thread.s per core:/{tpc$2} END{print cores/tpc}5.4 内存实际频率和通道数与标称不符现象DDR4-3200标称搭配8通道STREAM只跑出100GB/s多一点效率不到60%。原因内存条插槽顺序不对8根内存只工作在了4通道或者BIOS没开XMP/EXPO内存跑在默认的2400MT/s档位还有可能是不同容量的条子混插触发了Flex Mode。解决用dmidecode查每根条的Speed和Configured Memory Speed对照主板手册确认插槽所属通道。BIOS里恢复成标称频率后再跑一轮对比。物理机上可以拔掉一半内存跑如果跑分几乎不变说明当前配置的内存通道本来就没喂满或者通道数冗余插槽顺序需要调整。5.5 对比基准不统一导致结果误判现象同一台机器内核升级、BIOS更新前后跑STREAM分数差15%两台硬件完全相同的机器分数差6%查了半天发现是gcc版本不同。原因STREAM结果的噪声来源太多编译器版本、优化参数、数组大小、线程数、NUMA绑定策略、频率策略全都影响最终值。解决每次跑分把编译命令、CPU策略、频率温度记录到同一份测试日志里。内部建立基准时固定gcc版本和编译选项用脚本一键执行。跨平台对比时还有个细节数组大小不要盲目统一成同一个数字因为Cache更大的机器会因此占便宜更好的做法是各自按L3 Cache的4倍换算数组大小这样对比的才是内存子系统的真实能力。6. STREAM结果的应用与验证把跑分变成调优决策6.1 用STREAM验证内存配置变更的效果最常见的用法是BIOS调整前后各跑一次。我习惯把基准测试固化成一个脚本每次跑完自动生成带时间戳的结果文件#!/bin/bash # 简易STREAM基准脚本记录环境信息与跑分 LOGstream_$(date %Y%m%d_%H%M%S).log { echo CPU lscpu | grep -E Model name|CPU\(s\)|NUMA node echo MEM SPD sudo dmidecode -t memory | grep -E Speed:|Configured Memory Speed: echo STREAM OMP_PLACEScores OMP_PROC_BINDclose numactl --cpunodebind0 --membind0 ./stream } | tee $LOG这个脚本把环境信息和跑分一起落盘回看日志时能直接判断分数变化是来自内存配置改动还是来自系统环境漂移。每次改完内存时序或频率我都强制过一遍这个脚本。6.2 多轮跑分与波动判断单轮跑分容易踩到随机干扰我一般跑3轮取中间值for i in 1 2 3; do OMP_PLACEScores OMP_PROC_BINDclose \ numactl --cpunodebind0 --membind0 ./stream | grep Triad done三轮结果相差在2%以内可以接受超过3%就要查频率、温度和后台负载来源。别取最大值当结论取中间值更稳。6.3 结合perf与业务负载验证STREAM是平台能力参考最后还是要落到实际业务。数据库或数据仓库压测场景里我会用perf看实际程序对访存的消耗perf stat -e task-clock,cycles,instructions ./your_benchmarkSTREAM告诉你系统上限能跑多快perf和业务压测告诉你业务实际用了多少带宽两者放在一起才能判断“瓶颈在内存带宽”是硬瓶颈还是业务侧没用好。结尾收一下我有一次为了压榨性能把一台数据库服务器的内存频率往上拉了一档没跑STREAM就直接上生产晚高峰时系统响应明显劣化回滚配置一查果然是内存带宽反而下降了。从那以后每次调内存频率或加内存条我都先跑一轮三轮循环的STREAM基准确认Triad稳定且没有明显回落才放行。这个习惯帮我避开了好几次“看似配置升级、实则性能回退”的情况希望帮到你。本文还有配套的精品资源点击获取
返回列表