资讯动态

Folly 自适应基准测试模式(Adaptive Benchmark Mode)深入指南:消除微基准中的系统噪声

发布时间:2026/9/10 7:05:50 来源:尧图企业网站定制
Folly 自适应基准测试模式Adaptive Benchmark Mode深入指南消除微基准中的系统噪声【免费下载链接】follyAn open-source C library developed and used at Facebook.项目地址: https://gitcode.com/GitHub_Trending/fol/folly自适应基准测试模式--bm_modeadaptive是 Folly 基准测试框架见 folly/Benchmark.cpp提供的一种噪声鲁棒的测量方案它将所有 benchmark 的短测量片段随机交错执行直到每个基准的结果同时满足稳定split-half 一致与精确置信区间足够窄两个条件。本指南围绕 folly/docs/BenchmarkAdaptive.md 展开结合源码实现说明其用法、收敛原理、适用边界与统计设计帮助你在嘈杂虚拟机或共享 CI 机器上获得公平、可复现的相对性能对比。什么是自适应模式传统的 best-of 模式将每个 benchmark 一次性完整跑完然后报告观测到的最小时间。这在安静的系统上没有问题但在噪声很大的虚拟机VM上它可能失效——例如 benchmarkA恰好在系统慢的时段运行B恰好在快的时段运行两者的相对性能就被扭曲了。自适应模式的做法完全不同将所有 benchmark 的短测量片段以随机顺序交错执行使所有 benchmark 经历几乎相同的系统状态序列持续采样直到结果同时满足稳定不振荡与精确95% 置信区间足够窄两个条件不再依赖运气好的最小值而是报告目标分位数默认 p33的估计值。自适应模式能给你什么运行内精度Within-run precision同一轮运行中的各个 benchmark 在相同系统条件下被公平比较消除了相关噪声的干扰。测量稳定性Measurement stability当检测到系统状态振荡时继续采样直到前半段测量与后半段测量相互吻合。安静系统上更快的收敛Faster convergence on quiet systems在系统状态平稳时它比 best-of 模式更快地得到精确结果best-of 需要反复寻找最小值而自适应直接逼近目标分位数。自适应模式不保证什么绝对精度Absolute accuracy二进制布局变化、热效应以及持续超过单次运行的长期系统状态变化仍然可能偏移结果。运行间可复现性Run-to-run reproducibility今天收敛的结果可能与明天收敛的结果不同差异甚至可能超出--bm_target_precision_pct所暗示的范围。但实践中运行间的结果仍然非常接近。从源码结构看一个值得注意的事实是当自适应模式收敛时其结果与 best-of 模式的好运行即运气好的那几次高度一致。best-of 模式数值略低因为它目标是最小值自适应模式目标是一个分位数默认 33rd 百分位。总结自适应模式试图把运气从运行内比较中剔除代价是偶尔更长的运行时间。对于跨运行比较例如提交代码前后的对比应当让两个构建在相同条件下各跑一次依赖相对差异而非绝对值。快速开始命令行用法在 Folly 的 buck2 工作流中对任意 benchmark 目标启用自适应模式buck2 run //mode/opt //your:benchmark -- --bm_modeadaptive默认参数下安静系统上可以快速收敛。在嘈杂 VM 上请增大单 benchmark 的时间上限buck2 run //mode/opt //your:benchmark -- --bm_modeadaptive --bm_max_secs30所有 flag 的完整定义与说明见 folly/Benchmark.cpp。下面是自适应模式最核心的几个参数Flag默认值作用--bm_modebest-of测量模式可选best-of或adaptive--bm_target_percentile33.3报告的目标分位数0-10090可捕获尾延迟行为--bm_target_precision_pct0.4收敛精度阈值目标分位数 95% 置信区间宽度不超过估计值的该百分比--bm_max_secs1每个 benchmark 的最大测量秒数噪声系统建议 20-30--bm_min_secs0.1每个 benchmark 在可收敛前的最短测量秒数--bm_min_samples20每个 benchmark 在可收敛前的最少样本数--bm_slice_usec1000单个连续测量切片的微秒数低于 1000 有框架干扰风险--bm_verbosefalse输出收敛进度、采样轮数、baseline 统计等诊断信息--bm_quietfalse静默非关键诊断输出控制收敛阈值--bm_target_precision_pct--bm_target_precision_pct决定精确的门槛——测量值的95% 置信区间宽度不得超过估计值的该百分比。默认值0.4意味着对于一个 100ns 的估计值置信区间宽度必须小于 0.4ns。接近零时的 20ps 绝对下限当测量值接近零时相对精度失去了稳定意义因此源码中引入了一个绝对下限。参见 folly/detail/BenchmarkAdaptive.h// Near zero, allow a 20ps-wide 95% CI instead of requiring relative precision // that has no stable meaning. See BenchmarkAdaptive.md for calibration. inline constexpr double kBenchmarkPrecisionFloorNs 0.02;对应的预算计算函数folly/detail/BenchmarkAdaptive.hinline double precisionBudgetNs(double estimate, double targetPrecisionPct) { return std::max( kBenchmarkPrecisionFloorNs, targetPrecisionPct / 100.0 * estimate); }也就是说精度预算取max(20ps, targetPrecisionPct/100 × estimate)。文档记载在两组 benchmark 对比实验中20ps 清除了所有观测到的精度失败而 30ps 和 50ps 没有多清除任何精度失败——这是 20ps 被选作下限的实验依据。注意--bm_target_precision_pct同时也控制 split-half 稳定性容差见下文理解稳定性因此当 20ps 下限接管 CI 宽度判定时收紧该参数仍可能影响收敛行为——在该精度水平上二进制布局效应往往占主导。警惕一个已收敛的测量仍可能是错的——见下一节自适应模式无法检测的内容。其他实用开关加--bm_verbose查看采样轮数、收敛进度等内部统计对应verboseLogInitial/verboseLogFinal见 folly/detail/BenchmarkAdaptive.cpp。用--bm_min_secs在更高置信度与更短时间之间权衡——确保每个 benchmark 至少运行这么长时间从而平均掉短时噪声。--bm_min_samples默认 20保证每个 benchmark 至少积累 20 个样本确保分位数与 CI 估计有足够的数据点。模式相关的 flag 约束源码在 folly/Benchmark.cpp 中对 flag 组合做了校验值得注意的约束包括--bm_target_percentile、--bm_target_precision_pct、--bm_min_secs、--bm_min_samples只有在--bm_modeadaptive下才有意义best-of 模式下显式设置会直接报错退出--bm_min_iters、--bm_max_iters、--bm_max_trials仅适用于 best-of 模式--bm_estimate_time与 adaptive 模式不兼容adaptive 报告目标分位数语义不同--bm_perf_args在 adaptive 模式下不可用因为交错采样使 perf 无法界定区间--bm_min_secs不得超过--bm_max_secs--bm_slice_usec取代了已废弃的--bm_min_usec两者含义相同不可同时设置。顺带一提--bm_estimate_time计算的是 p25-p75 的几何均值是另一种测量策略速度更慢且对自适应模式的作者而言其效用并不明确——需要噪声鲁棒测量时优先用--bm_modeadaptive。自适应模式无法检测的内容长期系统状态变化持续时间超过单次运行的系统状态变化几十秒级别对自适应模式是不可见的。你可能得到一次稳定且收敛的运行但它与下一次运行并不吻合。跨运行比较时的建议例如把你的提交与 baseline 对比尽量在同一会话中背靠背运行两个构建间隔数分钟到数小时重复运行捕捉长期漂移比较 benchmark 之间的相对差异而不是绝对值条件允许时在专用基准测试硬件上运行。二进制布局变化微基准对指令缓存对齐高度敏感。看似无害的代码改动就可能把计时移动几个百分点——远超你要求的精度。文档中的实测案例在verboseLogFinal()中加入下面这段死代码当--bm_verbose关闭时永远不会执行在--bm_modeadaptive --bm_max_secs20 --bm_target_precision_pct0.03条件下稳定地偏移了计时结果std::ostringstream rawOss; rawOss \n[bm_raw_samples]; for (const auto s : states) { rawOss \n s.name; for (const auto sample : s.samples) { rawOss \t sample.first; } } LOG(INFO) rawOss.str();这是微基准测试固有的问题。一个微基准不存在独立于其二进制布局的真实运行时间。自适应模式改善的是运行内的相对比较不同 benchmark 看到相同的系统状态但绝对值会随构建而漂移。启示不要执着于 0.1% 的精度目标。二进制布局效应轻易就能引入 5-20% 的方差。用自适应模式获得稳定、精确的相对比较而不是完美的绝对测量。当你的系统状态不佳时有时你会看到 benchmark 一直卡在振荡状态。即便它们最终收敛数字也值得怀疑——热节流、吵闹的邻居、后台任务都可能是元凶。如果看到持续的[unstable]警告源码中由formatSampleStats输出见 folly/detail/BenchmarkAdaptive.cpp稍后再试或换一台更安静的机器考虑这种不稳定是否是真实的你的代码本身是否有方差如果大多数 benchmark 都收敛了、只有某一个不收敛那么该 benchmark 本身可能固有噪声数据相关的分支、分配等用--bm_min_secs强制更长的运行平均掉短期噪声或者干脆接受超时——至少你知道自己的数字是可疑的。此外框架在运行中会周期性输出中间报告默认每 5 秒一次见 folly/detail/BenchmarkAdaptive.cpp 的kIntermediateResultInterval并给未收敛的 benchmark 标注[gathering minimums]/[imprecise]/[unstable]状态见formatIntermediateReport。若最终超时框架会打印[RUN INCOMPLETE]标记——folly/tool/benchmark_ab.py 中的log_has_incomplete_run()正是通过匹配该标记来判定一轮基准运行是否不完整def log_has_incomplete_run(log_path: Path) - bool: return [RUN INCOMPLETE] in log_path.read_text(encodingutf-8, errorsreplace)超时原因被细分为三类见checkAllDone的分类逻辑folly/detail/BenchmarkAdaptive.cppExceeded max_secs (minimums unmet)、Exceeded max_secs (unstable)、Exceeded max_secs (imprecise)并在最终汇总中分别报告 Did not converge 与 Did not meet minimums。为什么用 33rd 百分位默认的--bm_target_percentile33.3是一个启发式选择不是科学结论。理由p0最小值给出过度乐观的估计。例如它可能掩盖由低级别系统争用导致的代码行为异常。p50中位数更稳健但会被瞬时系统减速所偏置。p33试图成为好运行的中位数——过滤掉顶端的噪声同时保持现实性。低分位数的风险如果你的代码呈现双峰分布快速路径 vs. 慢速路径低分位数可能掩盖慢速路径。如果你关心尾延迟可以尝试--bm_target_percentile90等更高的值。与 best-of 模式的对比best-of 模式报告 p0所有迭代中的最小值。自适应的 p33 会略高但在噪声系统上更可复现。在安静条件下比较同一二进制的 adaptive 与 best-of 时会观察到少量差异——这是预期的。测试代码folly/detail/test/BenchmarkAdaptiveTest.cpp中的SortedSamples::percentile使用线性插值计算分位数percentileCI用二项分布的正态近似估计分位数的标准误差这为 p33 等任意分位数提供了可靠的置信区间。理解稳定性Stability一个 benchmark 是稳定的当且仅当前半段与后半段样本的估计相互一致。判定流程把样本按时间顺序切分为两半分别计算各自的分位数估计与置信区间。稳定意味着前半段估计落在后半段置信区间内后半段估计落在前半段置信区间内。例子200 个样本之后前半段 p33 4.2ns ± 0.1ns后半段 p33 4.3ns ± 0.15ns——稳定因为每个估计都落在对方区间内。绝不丢弃样本。随着样本数增长置信区间收缩两半最终必须一致或者超时证明你的系统太吵。直觉如果系统状态平稳两半会收敛到同一个真实值当它们不一致时说明有东西变了——继续采集直到它们一致。实现细节computeStabilityStatsfolly/detail/BenchmarkAdaptive.cpp要求至少 4 个样本并使用带 epsilon 容差的判定double epsilonNs std::max( // 1 picosecond is below typical CPU clock resolution, so any // instability of that amount is spurious. 0.001, // Inter-half drift below 1/2 of the target precision cant push the // final estimate outside the precision budget. targetPrecisionPct / 100.0 * 0.5 * std::min(ci1.estimate, ci2.estimate));即1ps 以下的不稳定是虚假的低于 CPU 时钟分辨率前后半段漂移低于目标精度的 1/2 时不会把最终估计推出精度预算之外因此也算稳定。测试 StabilityStatsTest.StableWhenDriftBelowTargetPrecision 验证了这一点0.05% 的漂移在 0.4% 目标精度下稳定在 0.01% 目标精度下不稳定。稳定 ≠ 准确。你可以对错误的数字得到稳定、精确的测量例如二进制布局、贯穿全程的热节流等造成的结果。但一个不稳定的测量则肯定是不可靠的。设计选择统计稳健性自适应模式目标是分位数而非均值。虽然计算成本高于均值/标准差但顺序统计量order statistics对离群值稳健且不假设分布形态。侧记实现既没有用StreamingStats.h在线均值/标准差也没有用TDigest.h近似分位数——因为不需要原因见下。分位数 CI 估计器是对二项分布的正态近似实践中样本量总是足够多使这一近似成为好选择。相关实现与单元测试见 folly/detail/BenchmarkAdaptive.h 和 folly/detail/test/BenchmarkAdaptiveTest.cpp。低开销大多数 CPU 时间应该花在 benchmark 本身而不是框架上。为达到这一目标的关键选择罕见的收敛检查每 150ms 检查一次kCheckInterval而不是每个样本都检查。分位数 CI 所需的 O(n log n) 排序被摊薄使TDigest.h这类复杂机制对本场景成为多余。降采样的基线测量空循环基线baseline很便宜约 0.3ns/iter每 8 轮跑一次suspender 基线BenchmarkSuspender的开销较贵约 35ns/iter每 2 轮跑一次。对应源码常量kBaselineSampleInterval 8与kSuspenderSampleInterval 2folly/detail/BenchmarkAdaptive.cpp且两个基线值会被缓存供后续样本复用。测量切片每个 benchmark 每次采样运行约bm_slice_usec默认 1ms。这个长度足以摊薄框架开销并避免函数调用边界带来的计时噪声。算法概览自校准的迭代次数每个 benchmark 从iterCount1开始在每次采样后自我调整使每个测量切片耗时接近bm_slice_usec。这避免了单独的校准阶段——校准阶段容易受冷启动效应缺页、TLB 未命中影响可能把iterCount锁死在 1 导致整轮无效。校准函数recalibrateIterCountfolly/detail/BenchmarkAdaptive.h是单调递增的慢采样不会减小迭代次数而 10ns 以下的微小耗时则直接翻倍而非相除。测试 RecalibrateIterCountTest 覆盖了这些边界。注bm_slice_usec应足够高以避免框架代码干扰——默认的 1ms 是合理下限100μs 就已经出现严重干扰。交错采样主循环SamplingLoop::runfolly/detail/BenchmarkAdaptive.cpp伪代码while not converged: measure baseline (every 8 rounds) measure suspender baseline (every 2 rounds) for each benchmark (random order): measure calibrated iterations, record sample, recalibrate check convergence every 150ms每一轮中未收敛的 benchmark 以随机顺序std::shuffle 线程本地 PRNG各采样一次保证所有 benchmark 获得平等的采样次数——测试 AdaptiveTest.EqualSamplingAcrossBenchmarks 专门验证了这一点。我们完成了吗对每个 benchmark收集最少样本数与最短测量时间minSamples/minSecs对应hasMetMinimums检查稳定性split-half 一致性对应isStable检查精确性CI 宽度 ≤ max(相对目标, 20ps 下限)对应isPrecise三个条件全部满足才停止hasConverged。超时时报告不完整运行而非收敛checkDonefolly/detail/BenchmarkAdaptive.cpp。测试 AdaptiveTest.HighVarianceHitsTimeout 验证了高方差数据最终触发maxSecs超时AdaptiveTest.MinSamplesGuaranteed 验证了即使常值数据立即收敛也会采集满minSamples个样本。第 4 阶段报告减掉基线空循环基线 suspender 开销见runAndAddSample中的逐样本调整逻辑folly/detail/BenchmarkAdaptive.cpp负值会被钳制为 0然后交给既有显示逻辑输出结果表。用户计数器UserCounters取自与分位数估计最接近的那个样本countersForEstimate测试 AdaptiveTest.UserCountersFromPercentileMatchedSample 验证了这一行为。总结与最佳实践在噪声环境VM、CI下优先使用--bm_modeadaptive并搭配--bm_max_secs20-30做提交前后对比时让新旧两个构建在同一会话背靠背运行比较相对差异不要追求 0.1% 的绝对精度——二进制布局效应会带来 5-20% 的方差自适应模式的价值在于稳定、精确的相对比较看到持续的[unstable]先怀疑系统而非代码必要时用--bm_min_secs拉长采样或换安静机器需要捕获尾延迟时用--bm_target_percentile90之类的高分位数默认 p33 是好运行的中位数的实用启发式。相关源码与测试核心实现 folly/detail/BenchmarkAdaptive.h 与 folly/detail/BenchmarkAdaptive.cppflag 定义与校验 folly/Benchmark.cpp单元测试 folly/detail/test/BenchmarkAdaptiveTest.cpp以及依赖 [RUN INCOMPLETE] 标记判定运行完整性的对比脚本 folly/tool/benchmark_ab.py。【免费下载链接】follyAn open-source C library developed and used at Facebook.项目地址: https://gitcode.com/GitHub_Trending/fol/folly创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价