你的基准测试在骗你(而这个148-Star Crate知道原因)
Source: Dev.to
(请提供您希望翻译的正文内容,我将为您翻译成简体中文,同时保持原有的格式、Markdown 语法和技术术语不变。)
概览
Microbenchmarks 会产生误导——不是出于恶意,而是结构性的问题。你编写一个紧凑的循环,测量一千次,比较两个实现,然后宣告胜者。但你的 CPU 在第二次运行时出现了热降频,或者操作系统在基准测试进行到一半时调度了后台进程,或者内存分配器在两次运行之间因你去吃午饭再回来而产生了不同的碎片化。
大多数基准测试框架通过收集更多样本并寄希望于统计方法来解决这个问题:将运行次数从一千提升到一万,剔除异常值,计算置信区间。这确实有帮助,但它只是对根本问题的临时修补:你在不同时间、不同系统条件下测量了 baseline 和 candidate。
如果可以避免这种情况怎么办?
Tango 是由 Denis Bazhenov 用 Rust 编写的微基准测试框架,围绕一个简单的理念:将 baseline 和 candidate 同时运行,而不是顺序运行。
Baseline → candidate → baseline → candidate … 全部在同一进程中进行,每次迭代交替执行。
- 热漂移对两者影响相同。
- 调度抖动对两者影响相同。
当你比较结果时,比较的两者在相同的时刻经历了相同的系统条件。项目将此称为 “paired testing.” 它能够产生更紧凑的置信区间,并比传统的顺序基准测试产生更少的误报。
Stars: ~148(撰写时)
License: MIT(默认)
项目概述
| 项目 | 细节 |
|---|---|
| 名称 | tango |
| 星标 | ~148 |
| 维护者 | 单人开发者,积极提交 |
| 代码健康 | 小巧、密集、组织良好 |
| 文档 | 可靠的 README 包含方法论说明;API 文档较薄 |
| 贡献者体验 | 架构清晰,维护者响应及时,接受贡献 |
| 值得使用吗? | 是的,只要你进行基准测试并关注结果的稳定性 |
整个工作空间约 3,900 行 Rust,核心 tango-bench crate 大约 3,350 行——对于它提供的功能来说已经很小。其架构合理,行数也得到了证明。
技术亮点
成对测试实现
- 通过
libloadingcrate 实现 动态库加载。 - 基准测试编译为 dylib;Tango 在同一进程中加载 两个副本。
- 在 Linux 上进一步使用 GOT/PLT 补丁(使用
goblin进行 ELF 解析)来拦截函数调用。 - 在 Windows 上则补丁 导入地址表 (Import Address Table)。
这是真正的系统编程,而不是围绕 std::time::Instant 的薄包装。
基准测试 API
benchmark_fn("my_algorithm", |b| {
b.iter(|| my_function(1000))
});
度量选择 是通用的,通过 turbofish 语法指定。Metric trait 只有一个方法:
pub trait Metric {
fn measure_fn(f: impl FnMut()) -> u64;
}
闭包被包装、测量,返回结果。该 trait 在编译时单态化,因此在热点路径中 没有 v‑table 调度。
- WallClock 默认使用
std::time::Instant(或在启用hw-timer特性时直接使用rdtscp)。 - 用户可以为每个基准测试切换度量方式:
b.metric::().iter(|| …);
依赖
| Crate | 用途 |
|---|---|
clap | CLI 解析 |
rand | 打乱迭代顺序 |
libc / windows | 平台特定调用 |
goblin / scroll | ELF 解析与补丁(Linux) |
alloca | 栈分配的采样缓冲区(避免分配噪声影响测量) |
没有不必要的臃肿。
粗糙之处
- 除了 README 之外的 API 文档较为稀少。
cli.rs中仍有一个 Clippy 警告(函数参数过多)。- 对核心统计与测量代码的测试覆盖率良好,但对 CLI 和 dylib 加载路径的覆盖相对薄弱——这在专注的个人项目中很常见。
最近的进展:Metric 特性
当 Tango 在 PR #60 中加入 Metric 特性时,它只提供了一个实现:WallClock。早前的 PR(#57)提议将默认计时器切换为 clock_gettime(CLOCK_THREAD_CPUTIME_ID) 以获取每线程 CPU 时间,但维护者正确地回绝了:CPU 时间 ≠ 墙钟时间(sleep(100 ms) 会记录 100 ms 的墙钟时间,但 CPU 时间几乎为零)。
有了可插拔的 Metric 特性,两者可以共存。
我的贡献:CpuTime 指标
- Unix – 使用
clock_gettime(CLOCK_THREAD_CPUTIME_ID)(纳秒精度)。 - Windows – 调用
GetThreadTimes(GetCurrentThread())并将用户时间与内核时间相加。
没有新增 crate;实现位于 cfg 属性后面,镜像 WallClock。此更改涉及 两个文件,共约 115 行(含测试)。
集成测试(PR #72)
#[test]
fn cpu_time_vs_sleep() {
// Sleep 50 ms → low CPU time
// Busy loop → high CPU time
// Assert busy loop reports ≥10× more CPU time than sleep
}
该测试展示了该指标的用途:thread::sleep 消耗墙钟时间,但不消耗 CPU 时间。
展望
Tango 面向那些进行基准测试并且因结果不一致而受挫的 Rust 开发者。如果你曾经看到过 5 % 的回归,结果却是笔记本的风扇启动导致的,那么配对测试的方法正好可以解决这个问题。
项目的进展方向很明确:
- 最近已落地
Metrictrait。 - 正在进行异步基准测试的支持。
- 维护者对 PR 和 issue 进行细致的交流。
它 不是一个停滞的副项目——它正在积极演进,清晰的架构应当能够支撑进一步的扩展。
还有哪些可以推动它进一步发展?
- 更全面的 API 文档(例如为公共 crate 提供 Rustdoc)。
- 为 Windows 特定的加载/打补丁路径增加测试覆盖。
- 更多内置指标(例如
InstructionsRetired、CacheMisses)。 - 社区驱动的插件,用于自定义统计分析。
如果你对 Rust 代码进行基准测试并且在乎结果的稳定性,试试 Tango 吧。
评测炸弹 #4
更多指标(比如通过 perf_event_open 的指令计数),更好的 API 文档,以及更广泛的认知。
配对测试方法是一种一旦理解就会让顺序基准测试显得明显错误的理念。应该让更多人了解它。
如果你在对 Rust 代码进行基准测试,去看看 tango:
- 阅读 README 中的 Methodology(方法论)章节。
- 运行其中一个示例。
- 即使你目前仍在使用 Criterion,配对测试方法也值得了解。
如何参与
- ⭐️ 给仓库加星。
- 在真实的基准上尝试它。
- 挑一个未解决的 issue 来做。
下面是我添加 CpuTime 的 PR,如果你想看看小贡献的样子:
[PR #: Add CpuTime metric](https://github.com///pull/)