Ruff 性能微基准测试实战:ruff_benchmark crate 的运行、回归对比与源码解析
ruff_benchmark 是 Ruff 仓库中专门用于在单个文件维度上对 linter 与 formatter 进行微基准(micro-benchmark)测试的 Criterion/Divan crate,也是 CONTRIBUTING.md 所描述的基准测试体系里"逐文件、逐代码路径"层面的关键一环。本文以 crates/ruff_benchmark/README.md 为主线,结合 CONTRIBUTING.md 的基准章节与各 bench 源码,完整讲解如何运行基线保存、回归对比、按模块过滤等命令,并剖析其内部测试文件、基准分组、特性开关与防抖设计,帮助你在改造 Ruff 代码后科学地验证"有没有变慢"。
概览:ruff_benchmark 在 Ruff 性能体系中的定位
Ruff 的性能验证分为三个层面(详见 CONTRIBUTING.md 的 "Benchmarking and Profiling" 一节):
- CPython 全仓库基准:用
hyperfine比较 Ruff 与同类工具在 CPython 代码库上的端到端耗时; - 微基准(microbenchmarks):由
ruff_benchmarkcrate 在个体文件上分别运行 lexer、parser、linter、formatter,并在 PR 上自动执行,用于及早发现具体代码路径的回归; - 性能剖析(profiling):借助微基准或真实项目目录做火焰图等分析。
ruff_benchmark 承担第 2 项。它的自述非常简洁(见 crates/ruff_benchmark/README.md):
The
ruff_benchmarkcrate benchmarks the linter and the formatter on individual files.
本 crate 定位为开发/CI 专用(publish = false,描述为 "Ruff Micro-benchmarks",见 Cargo.toml),其正式使用说明(baseline 对比、critcmp PR 对比、常用 tips)位于 CONTRIBUTING.md 的 Microbenchmarks 小节,README 中的命令正是它的快速入口。
快速上手:三条核心命令
进入仓库根目录后,可用如下命令立即测量当前工作区代码的解析与检查性能。其中 -- 之后的所有参数都会原样透传给 Criterion 基准框架:
# 1. 先在 "baseline" 上运行一次,把结果保存为名为 main 的基线
cargo bench -p ruff_benchmark -- --save-baseline=main
# 2. 之后(例如改动代码后)再次运行,与刚才保存的基线对比
cargo bench -p ruff_benchmark -- --baseline=main
# 3. 只运行 lexer 相关的基准(可按基准名过滤)
cargo bench -p ruff_benchmark lexer -- --baseline=main
第一条命令会运行 crate 中默认特性下可用的全部基准并存储基线,第二条命令则在每次迭代后由 Criterion 自动给出与 main 基线的统计对比结论("improved/regressed")。第三条展示了按名称过滤的能力:lexer 会命中名为 lexer 的 bench 目标(具体过滤规则见下文"按名称过滤"一节)。
更快的入口:cargo benchmark 别名
如果你只想快速盯住两条最核心的代码路径(linter 与 formatter),仓库在 .cargo/config.toml 中预置了别名:
cargo benchmark
它等价于(CONTRIBUTING.md 中明确给出):
cargo bench -p ruff_benchmark --bench linter --bench formatter --
也就是说,cargo benchmark 与全量 cargo bench -p ruff_benchmark 的区别在于:后者还会编译并运行 parser、lexer、ty 类型检查等其余 bench 目标(各目标的 required-features 与特性开关详见下文)。
运行前注意:基准对噪音高度敏感。仓库建议在 CPU 基本空闲时测量(关闭浏览器等后台程序),若 CPU 支持性能模式(performance mode)也应开启,尤其是针对耗时很短的基准。这与 CONTRIBUTING.md 中的 Note 一致。
被测对象:代表性测试文件与 TestFile 抽象
微基准的载体是若干具有代表性的 Python 源文件。它们在 src/lib.rs 中被定义为静态 TestFile,通过 include_str! 在编译期嵌入(即测试数据随 crate 一起编译,运行时无磁盘 IO):
| 常量 | 文件名(resources 目录) | 选取意图 |
|---|---|---|
NUMPY_GLOBALS |
numpy/globals.py |
NumPy 源码片段,典型科学计算代码风格 |
UNICODE_PYPINYIN |
pypinyin.py |
大量 Unicode/中文字符串处理,考验字符串与注释路径 |
PYDANTIC_TYPES |
pydantic/types.py |
类型注解密集的现代 Python(对 ty 类型检查尤其相关) |
NUMPY_CTYPESLIB |
numpy/ctypeslib.py |
含 ctypes 用法的 NumPy 模块 |
LARGE_DATASET |
large/dataset.py |
大体积测试数据(源自 mikeio 项目测试文件,见 src/lib.rs 中的来源注释) |
这些资源文件真实存在于 crates/ruff_benchmark/resources/ 下(另有 tomllib/、typeis_narrowing.py 等供 ty 相关基准使用的文件)。TestFile 封装了 name 与 code,并提供 code()、name()、path() 三个方法;其中 path() 会把名称解析回 resources 目录下的真实路径,供 linter 基准按扩展名推导源码类型(src/lib.rs)。linter、parser、lexer、formatter 四个基准共享完全相同的 5 个测试文件(各自 create_test_cases() 返回同样列表),这使不同阶段的耗时具有可比性。
基准目标与特性开关(feature matrix)
crates/ruff_benchmark/Cargo.toml 声明了 5 个特性,default 聚合了其中 4 个:
| feature | 含义 | 引入的 bench 目标 |
|---|---|---|
ruff_instrumented |
打桩测时 Ruff 各阶段(lexer/parser/linter/formatter),基于 Criterion | linter、lexer、parser、formatter |
ty_instrumented |
打桩测时 ty(类型检查器)内部,基于 Criterion | ty、ty_constraint_set |
module_resolution |
模块解析基准,基于 Divan | module_resolution |
ty_walltime |
ty 在真实项目上的墙钟耗时,基于 Divan | ty_script、ty_walltime |
codspeed |
切换 Criterion 兼容层以支持 CodSpeed 平台 | 无 |
每个 [[bench]] 目标都设置了 harness = false 与对应的 required-features(Cargo.toml)。因此:
- 使用默认特性执行
cargo bench -p ruff_benchmark时,9 个 bench 目标(linter、lexer、parser、formatter、ty、ty_constraint_set、module_resolution、ty_script、ty_walltime)全部参与; - 若需要裁剪(例如只测 Ruff 不测 ty),可
cargo bench -p ruff_benchmark --no-default-features --features ruff_instrumented。
两套测时框架的取舍也体现在源码中:Ruff 工具链(linter 等)与 ty 打桩基准使用 Criterion(因为要配合 --save-baseline/--baseline 做统计回归),而 module_resolution、ty_script、ty_walltime 三个 bench 使用 Divan。
Criterion / CodSpeed 兼容层
crates/ruff_benchmark/src/criterion.rs 是一个极简的 re-export 层:未启用 codspeed 特性时直接转发 Criterion 全量 API,并把 BenchmarkGroup 固定为 criterion::BenchmarkGroup<'a, WallTime>;启用 codspeed 后则转发 codspeed_criterion_compat。代码注释说明这样做的原因:CodSpeed 并非所有平台都支持,因此需要兼容层按需切换后端。各 bench 源文件统一 use ruff_benchmark::criterion; 而非直接依赖 criterion,正是为这一切换预留的抽象。
逐个拆解:五个 Ruff 微基准的实现细节
linter 基准(benches/linter.rs)
benches/linter.rs 定义了三个 Criterion 分组,覆盖三类规则集:
linter/default-rules:使用LinterSettings::default(),即开箱即用的默认规则集;linter/all-rules:收集RuleSelector::All的全部规则(preview 关闭);linter/all-with-preview-rules:启用PreviewMode::Enabled,覆盖含 preview 规则的全集。
代码层面有几点值得借鉴的工程细节:
- 解析移出计时区:先用
parse_module解析一次,随后用b.iter_batched(|| parsed.clone(), ..., criterion::BatchSize::SmallInput)计时——每次迭代仅克隆已解析的 AST 并执行lint_only,传入ParseSource::Precomputed(parsed)(linter.rs),从而把"测 linter"和"测 parser"解耦,避免重复解析污染 lint 耗时。 - 剔除 IO 型规则:
disable_io_rules显式关闭ShebangMissingExecutableFile与ShebangNotExecutable两条与文件系统相关的规则(linter.rs),原因注释写明"Disables IO based rules because they are a source of flakiness",即防止基准因 IO 而抖动。 - 吞吐量标注:对每个 case 调用
Throughput::Bytes(...),让 Criterion 额外报告 MiB/s 等吞吐指标。
formatter 基准(benches/formatter.rs)
benches/formatter.rs 在 formatter 分组中测量格式化全流程。关键调用链:
parse(code, ParseOptions::from(Mode::Module))得到带 token 的语法树;- 由 tokens 构建
TriviaRanges(注释/空白等 trivia 区间); - 通过文件扩展名构造
PyFormatOptions,并显式with_preview(PreviewMode::Enabled); format_module_ast(...)生成格式化中间表示,最后print()输出。
这里同样把解析步骤放在 b.iter() 之外的 setup 阶段(formatter.rs),计时区只包含真正的格式化与打印。
lexer 基准(benches/lexer.rs)
benches/lexer.rs 的计时逻辑最为纯粹:用 lexer::lex(code, Mode::Module) 建立词法迭代器,然后反复 next_token() 直到遇到 EndOfFile;若出现 Unknown token 则直接 panic(保证输入是合法 Python)。整段迭代都在被测时间之内,测的是词法切分的原始吞吐。
parser 基准(benches/parser.rs)
benches/parser.rs 对每个文件反复调用 parse_module(...) 并断言语法有效。一个细节是使用 b.iter_with_large_drop(|| parse_module(...))(parser.rs)——该 API 会把大对象的析构成本也计入迭代,适合测量会产生巨大 AST 分配的 parse 阶段。
关于 allocator 的一致性设置
为避免"内存分配器不同导致基准失真",linter/lexer/parser/formatter 四个 bench 文件开头都做了相同的全局分配器替换:Windows 上使用 mimalloc,其余平台(非 OpenBSD 且架构为 x86_64/aarch64/powerpc64/riscv64)使用 tikv-jemallocator(见 Cargo.toml 与各 bench 文件)。更重要的是,jemalloc 会通过导出 _rjem_malloc_conf 将 dirty_decay_ms 与 muzzy_decay_ms 都设为 -1(linter.rs),代码注释解释了原因:默认 10s 的 decay 可能表现为随机性的慢分配,而基准场景并不需要把未分配页面归还给 OS,禁用 decay 后测量更稳定。
ty 相关基准:类型检查器的微基准与真实项目耗时
仓库中 ty(类型检查引擎)处于开发阶段,其性能验证同样收敛到本 crate。这类基准分成两组:
打桩微基准 ty 与 ty_constraint_set:使用 Criterion。ty 基准(benches/ty.rs)包含两路测试:一是以标准库 tomllib 的 4 个文件为微场景的 FileCase(数据来自 resources/tomllib/),二是从 ty 生态固定一批真实仓库做端到端检查。该文件还体现了"基准也要防回归"的思想:通过 KeyDiagnosticFields(诊断 ID、代码、范围、消息、严重级别构成的元组)配合 EXPECTED_TOMLLIB_DIAGNOSTICS 断言诊断输出符合预期(ty.rs),从而避免在基准内部使用 insta 快照机制。benches/ty_shared/ 目录(如 setup_micro_case、setup_rayon)被这些基准共享。
墙钟耗时 ty_walltime 与 ty_script:改用 Divan,直接面向真实项目。其基础设施位于 src/real_world_projects.rs,工作方式如下:
- 把目标仓库克隆/更新到
./target下的缓存目录(顶层常量TY_ECOSYSTEM_PIN = "2026-06-17T06:46:32Z"用于固定依赖时间窗,防止外部依赖漂移引入抖动); - 对带依赖的项目用
uv创建虚拟环境并安装依赖; - 可选:将整个项目结构复制进内存文件系统,减少基准中的 IO 噪音;
- 随后构造
ProjectDatabase并执行基准。
例如 benches/ty_walltime.rs 中,每次迭代会 ProjectMetadata::discover 发现项目、覆写 Options(指定 Python 版本、.venv 路径等),再以 OsSystem 打开真实文件系统进行全量检查计时。
模块解析基准 module_resolution:benches/module_resolution.rs 测量的是"随着额外搜索路径(extra_paths)数量增长,冷模块解析查询批次的耗时变化"。它在内存文件系统(TestSystem)中构造从 0 到 N 个 /extra/p{n} 目录,并用三类查询考察解析器行为:预先 seed 的目标模块(命中缓存/逐级查找)、不存在的模块名(练习 stub-overlay 发现后的回退)、标准库模块名(os、sys、typing 等)。同一批次内的查询共享底层 resolver 缓存,用于评估"搜索路径越多,冷解析越慢"这一核心问题。
回归驱动的开发流程:baseline 保存与对比
这是 crates/ruff_benchmark/README.md 直接指向 CONTRIBUTING.md 的核心用法。Ruff 的微基准建立在 Criterion 之上,推荐的"基准驱动开发"工作流为:
# 1. 在改动前的代码(例如 main 分支)保存基线
cargo bench -p ruff_benchmark -- --save-baseline=main
# 2. 修改代码后反复迭代对比
cargo bench -p ruff_benchmark -- --baseline=main
每次对比后 Criterion 都会打印该基准相对基线的统计变化(improved/regressed 及置信区间),据此即可判断一次改动是否引入了可观测的退化。
按名称过滤基准
微基准数量多、耗时长,建议只跑关心的那部分。例如:
# 只运行 lexer 相关基准(对应 bench 目标名 lexer)
cargo bench -p ruff_benchmark lexer -- --baseline=main
在 Criterion 中,cargo bench -p ruff_benchmark <filter> 的 filter 会按分组/场景路径的子串匹配,因此既可以写 bench 目标名(如 lexer、parser、formatter、ty),也可以写更细的分组名(如 linter/default-rules 或 linter/all-rules,分组名定义见 benches/linter.rs)。例如只测默认规则集下的 lint 性能:
cargo bench -p ruff_benchmark "linter/default-rules"
常用调试开关
CONTRIBUTING.md 的 Tips 补充了两个降低等待成本的开关:
# 输出更精简(省略统计显著性信息)
cargo bench -p ruff_benchmark -- --quiet
# 更快出结果(但更容易受噪音影响)
cargo bench -p ruff_benchmark -- --quick
PR 场景:用 critcmp 生成美观的对比摘要
当需要向 PR 评审展示改动带来的提升/回退时,仓库推荐 --save-baseline 配合 critcmp 工具:
# 在 main 上保存一份基线
cargo bench -p ruff_benchmark -- --save-baseline=main
# 应用改动后保存另一份
cargo bench -p ruff_benchmark -- --save-baseline=pr
# 对比两份基线
critcmp main pr
critcmp 需要单独安装(README 指向其项目主页,可用如下命令安装):
cargo install --locked critcmp
微基准与剖析的衔接
微基准的另一个用途是作为 profiling 的固定输入,把热点分析收敛到可复现的样本上。仓库给出的两条路径都与 ruff_benchmark 直接相关:
- Linux + perf:先以
profilingprofile 编译(cargo bench -p ruff_benchmark --no-run --profile=profiling),再用perf record录制指定时长的基准执行(示例命令含--profile-time=1,即每种场景只跑 1 秒,详见 CONTRIBUTING.md 的 Profiling Projects 一节); - macOS + cargo-instruments:
cargo instruments -t time --bench linter --profile profiling -p ruff_benchmark -- --profile-time=1,-t time测墙钟时间,-t alloc则用于分析分配;还可附加单个测试文件名过滤器只剖析一个场景。
注意此类命令依赖仓库中定义的 profiling profile(更接近 release 且保留符号),运行前请确认当前 toolchain 支持。
小结:何时使用何种命令
| 目标 | 推荐命令 |
|---|---|
| 快速自检 linter + formatter 有无回归 | cargo benchmark |
| 全量微基准并保存/对比基线 | cargo bench -p ruff_benchmark -- --save-baseline=<name> / -- --baseline=<name> |
| 只测某一个阶段 | cargo bench -p ruff_benchmark lexer、parser、formatter、ty … |
| 精简/快速输出 | cargo bench -p ruff_benchmark -- --quiet、-- --quick |
| PR 对比摘要 | 分别 --save-baseline=main、--save-baseline=pr 后 critcmp main pr |
进一步阅读与源码入口:
- 基准快速说明:crates/ruff_benchmark/README.md
- 完整使用指南(含 alias、baseline 工作流、critcmp、profiling):CONTRIBUTING.md
- 别名配置:.cargo/config.toml
- 特性与 bench 目标声明:crates/ruff_benchmark/Cargo.toml
- 测试文件与
TestFile定义:crates/ruff_benchmark/src/lib.rs - 各基准实现:
linter、lexer、parser、formatter、ty、ty_walltime、module_resolution等源码位于 crates/ruff_benchmark/benches/ - 真实项目基准基础设施与 Criterion/CodSpeed 兼容层:crates/ruff_benchmark/src/real_world_projects.rs、crates/ruff_benchmark/src/criterion.rs
需要强调,这些基准衡量的是改动前后的相对回归,而非对外部工具的绝对结论;本文所有数据与结论均以当前仓库文档与源码为准。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00