news 2026/10/17 6:39:29

基准测试的陷阱:优化掉、频率漂移、缓存预热

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基准测试的陷阱:优化掉、频率漂移、缓存预热

写基准测试的人最容易犯的错,不是代码写错,而是「测的根本不是你以为在测的东西」。一个累加循环可能被编译器按 as-if 规则整段删掉,让你测出「接近 0 秒」的假象;一次运行里 CPU 可能正忙着升频,下一次又在降频节流,两次耗时根本不可比;第一轮遍历大数组时缓存是冷的,后面才是热的。这篇把这三个坑挖开,给你一套能落地的正确测量手法。

1. 陷阱一:结果没被观测,计算被优化掉

这是最隐蔽、也最伤人的一个。C++ 的 as-if 规则允许编译器做任何优化,只要程序的「可观测行为」不变。如果一个累加循环的结果从头到尾没被任何人读取,编译器就可以认为这段计算没有可观测效果,把它整个删掉。

官方文档:as-if 规则——「可观测行为」是优化边界,结果没人读就等于没发生。

看汇编就。下面两个函数,唯一的区别是第二个把结果喂给了do_not_optimize(一个空的内联汇编屏障):

template<classT>inlinevoiddo_not_optimize(Tconst&value){asmvolatile(""::"r,m"(value):"memory");}voidbench_discarded(intn){// 反例:sum 从未被读取longsum=0;for(inti=0;i<n;++i)sum+=i;}voidbench_kept(intn){// 正例:用 do_not_optimize 观测 sumlongsum=0;for(inti=0;i<n;++i)sum+=i;do_not_optimize(sum);}

g++ -O2编译后(godbolt,gcc 13.2)的汇编:

bench_discarded(int): ret ← 整个循环被删,只剩一条返回 bench_kept(int): test edi, edi jle .L6 movsx rcx, edi xor eax, eax xor edx, edx and edi, 1 je .L5 mov eax, 1 cmp rcx, 1 je .L4 .L5: lea rdx, [rdx+1+rax*2] add rax, 2 cmp rcx, rax jne .L5 .L4: ret .L6: xor edx, edx ret

bench_discarded只剩一条ret。如果你拿它测「这个循环要跑多久」,会得到「几乎 0 纳秒」的假数据。do_not_optimize的作用,是借内联汇编的"r,m"(value)约束告诉编译器「value 被用到了」,同时volatile让编译器不敢删这条 asm,于是循环被保留。

官方文档:GCC 内联汇编 Extended Asm——asm volatile与"r,m"约束的语义来源。

这个do_not_optimize写法来自 Google Benchmark 和 Chandler Carruth 的演讲,是事实上的标准手法(MSVC 上需换成_ReadWriteBarrier)。下面是它的最小可运行示例:

#include<cstdio>template<classT>inlinevoiddo_not_optimize(Tconst&value){asmvolatile(""::"r,m"(value):"memory");}intmain(){longsum=0;constexprintn=1000;for(inti=0;i<n;++i){sum+=i;}do_not_optimize(sum);// 观测 sum,防止循环被整体删掉std::printf("sum = %ld\n",sum);}
sum = 499500

2. 陷阱二:CPU 频率漂移与节流

就算循环没被删掉,两次运行之间还隔着「CPU 频率不稳定」这道坎。现代 CPU 有动态调频(dynamic frequency scaling):

CPU 频率漂移与节流(为什么耗时不可比) ═══════════════════════════════════════════════════════════════ turbo boost ──▶ 负载低时单核冲高频,跑一小段热代码特别快 降频节流 ──▶ 温度/功耗撞墙后,频率被拉低(thermal/power throttle) 后台抢占 ──▶ 别的进程抢 CPU,你的进程被切走,计时被拉长 ═══════════════════════════════════════════════════════════════ 结果:同一段代码,前 1 秒和后 1 秒测出的耗时可能差出几十个百分点, 单次计时几乎不可信。解法不是「测得更准」,而是「多轮取中位数」。

说明:这篇在在线沙箱里跑,无法控制 CPU 频率、也没有perf/taskset这类工具,
所以频率漂移这部分讲原理为主、不实测。真正做基准时用taskset绑核、关 turbo、
固定 governor,再谈数字。

这引出一条硬规矩:耗时类数字一律不写死。同一个循环,换台机器、换个负载,数字就变了;写进文章里只会误导半年后回看的自己。要下结论,用确定性的指标(循环次数、分配次数、汇编里有没有某条指令),耗时只用来做同一进程内的相对比较。

3. 陷阱三:缓存冷热,第一轮不算数

第三个坑藏在「第一轮」里。第一次遍历一个大数组时,数据还在主存,缓存是冷的,每一行都要真去内存取;第二轮开始数据已经在 L2/L3 里,是热的,速度完全不同:

#include<cstdio>#include<vector>#include<chrono>template<classT>inlinevoiddo_not_optimize(Tconst&value){asmvolatile(""::"r,m"(value):"memory");}intmain(){constexprstd::size_t n=8*1024*1024;// 8M 个 int,约 32MBstd::vector<int>data(n,1);longsum=0;autot0=std::chrono::steady_clock::now();for(std::size_t i=0;i<n;++i)sum+=data[i];// 冷缓存:第一遍autot1=std::chrono::steady_clock::now();doublecold=std::chrono::duration<double,std::milli>(t1-t0).count();t0=std::chrono::steady_clock::now();for(std::size_t i=0;i<n;++i)sum+=data[i];// 热缓存:第二遍t1=std::chrono::steady_clock::now();doublehot=std::chrono::duration<double,std::milli>(t1-t0).count();do_not_optimize(sum);std::printf("cold=%.3f ms hot=%.3f ms sum=%ld\n",cold,hot,sum);}
cold=~~ ms hot=~~ ms sum=16777216

要点:sum=16777216是确定性结果(两遍各累加 8M 个 1),说明循环确实跑完了、没被优化掉;而cold/hot的耗时在沙箱里不稳定、也可能被向量化抹平差异,所以只做相对演示、不写死具体毫秒数。真实场景里「冷缓存第一遍」通常会明显更慢,正确做法是先跑一遍预热轮,把缓存和分支预测器焐热,再开始正式计时。

4. 正确手法:多轮取中位数 + steady_clock

把三个坑合起来,正确的基准骨架长这样:

一次可信的基准测量流程 ═══════════════════════════════════════════════════════════════ 预热轮 ──▶ 焐热缓存、稳定频率 │ 多轮计时(每轮独立计时,用 steady_clock) │ 丢弃最慢/最快的离群值,取中位数(不是平均值) │ 用 do_not_optimize 确保结果被观测、计算没被删 ═══════════════════════════════════════════════════════════════

计时一定要用std::chrono::steady_clock。它是单调时钟,不会像system_clock那样被系统时间调整(NTP 校时、用户改时间)影响。完整示例:

#include<cstdio>#include<vector>#include<algorithm>#include<chrono>template<classT>inlinevoiddo_not_optimize(Tconst&value){asmvolatile(""::"r,m"(value):"memory");}intmain(){constexprintrounds=5;constexprintn=100000;std::vector<int>data(static_cast<std::size_t>(n),3);// 工作负载:数组求和std::vector<double>times;times.reserve(rounds);// 预热轮:焐热缓存,不计入结果{longwarm=0;for(inti=0;i<n;++i)warm+=data[i];do_not_optimize(warm);}longchecksum=0;for(intr=0;r<rounds;++r){autot0=std::chrono::steady_clock::now();longs=0;for(inti=0;i<n;++i)s+=data[i];do_not_optimize(s);autot1=std::chrono::steady_clock::now();times.push_back(std::chrono::duration<double,std::micro>(t1-t0).count());checksum+=s;}std::sort(times.begin(),times.end());// 排序后取中位数,抗离群值doublemedian=times[rounds/2];std::printf("rounds=%d median=%.1f us checksum=%ld\n",rounds,median,checksum);}
rounds=5 median=~~ us checksum=1500000

这段代码把三条规矩都落到了实处:steady_clock计时、多轮取中位数(checksum=1500000是确定性校验,证明每轮都真算了)、do_not_optimize防止数组求和被删。中位数比平均值更抗离群。某一轮恰好被系统抢占拖慢,平均值会被带偏,中位数不会。

三大陷阱与对策汇总:

陷阱症状对策确定性指标
结果没观测被优化掉汇编只剩ret,耗时≈0do_not_optimize观测结果汇编里有没有那条指令
CPU 频率漂移/节流同一段代码耗时忽快忽慢多轮取中位数、绑核、关 turbo循环次数
缓存冷热第一轮明显偏慢预热轮 + 正式计时预热后轮次的耗时

5. 延伸阅读

  • as-if 规则:编译器能删掉「结果无人读」的计算的理论依据
  • std::chrono::steady_clock:为什么计时要用单调时钟
  • GCC Extended Asm:do_not_optimize里asm volatile的语法来源
  • Compiler Explorer:写完基准先看汇编,确认循环没被删、该向量化的向量化了
  • C++ Core Guidelines:性能相关的 Per 系列规则

本知识库内的相关篇目:

  • 《SIMD 与自动向量化:march=native、__restrict 与 likely》 —— 自动向量化(auto-vectorization)让编译器把标量循环翻译成 SIMD 指令
  • 《PGO 与 LTO:profile-guided 与链接时优化》 —— LTO(链接时优化)把「跨编译单元」的优化推迟到链接期
  • 《C++ 缓存预热与 prefetch:什么时候该手写》 —— C++ 缓存预热与 prefetch,同体系里最该一起看的另一篇。

6. 一句话总结

基准测试的三大坑——结果没被观测会被 as-if 规则优化掉(用do_not_optimize防)、CPU 频率漂移让单次耗时不可比(多轮取中位数)、缓存冷热让第一轮失真(先预热)——核心心法是:耗时数字不写死、用确定性指标(循环次数、汇编指令、校验和)支撑结论,计时用steady_clock,结论只建立在同一进程内的相对比较上。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 6:38:19

wiliwili 完整安装指南:掌机与 PC 通吃的手柄 B 站客户端

wiliwili 完整安装指南&#xff1a;掌机与 PC 通吃的手柄 B 站客户端 【免费下载链接】wiliwili 第三方B站客户端&#xff0c;目前可以运行在PC全平台、PSVita、PS4 、Xbox 和 Nintendo Switch上 项目地址: https://gitcode.com/GitHub_Trending/wi/wiliwili wiliwili 是…

作者头像 李华
网站建设 2026/10/17 6:37:30

MIRIX: Multi-Agent Memory System for LLM-Based Agents

文章主要内容总结 本文介绍了一种名为MIRIX的多智能体记忆系统,旨在解决基于大语言模型(LLM)的智能体在记忆能力上的局限性。现有记忆系统多依赖扁平、范围狭窄的记忆组件,难以实现个性化、抽象化及长期可靠的用户信息召回,而MIRIX通过模块化设计和多智能体框架,重新定义…

作者头像 李华
网站建设 2026/10/17 6:37:13

MAME算法的各种密码分析方法全面盘点

MAME算法的各种密码分析方法全面盘点针对MAME&#xff08;一种面向硬件高效实现的压缩函数&#xff09;的密码分析&#xff0c;学术界已进行了多角度的评估。这些分析主要围绕其广义Feistel结构和MMO&#xff08;Matyas-Meyer-Oseas&#xff09;哈希模式展开&#xff0c;揭示了…

作者头像 李华