首页
/ Ruff 性能微基准测试实战:ruff_benchmark crate 的运行、回归对比与源码解析

Ruff 性能微基准测试实战:ruff_benchmark crate 的运行、回归对比与源码解析

2026-09-07 23:54:13作者:宣利权Counsellor

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" 一节):

  1. CPython 全仓库基准:用 hyperfine 比较 Ruff 与同类工具在 CPython 代码库上的端到端耗时;
  2. 微基准(microbenchmarks):由 ruff_benchmark crate 在个体文件上分别运行 lexer、parser、linter、formatter,并在 PR 上自动执行,用于及早发现具体代码路径的回归;
  3. 性能剖析(profiling):借助微基准或真实项目目录做火焰图等分析。

ruff_benchmark 承担第 2 项。它的自述非常简洁(见 crates/ruff_benchmark/README.md):

The ruff_benchmark crate 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 封装了 namecode,并提供 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 linterlexerparserformatter
ty_instrumented 打桩测时 ty(类型检查器)内部,基于 Criterion tyty_constraint_set
module_resolution 模块解析基准,基于 Divan module_resolution
ty_walltime ty 在真实项目上的墙钟耗时,基于 Divan ty_scriptty_walltime
codspeed 切换 Criterion 兼容层以支持 CodSpeed 平台

每个 [[bench]] 目标都设置了 harness = false 与对应的 required-featuresCargo.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_resolutionty_scriptty_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 显式关闭 ShebangMissingExecutableFileShebangNotExecutable 两条与文件系统相关的规则(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.rsformatter 分组中测量格式化全流程。关键调用链:

  1. parse(code, ParseOptions::from(Mode::Module)) 得到带 token 的语法树;
  2. 由 tokens 构建 TriviaRanges(注释/空白等 trivia 区间);
  3. 通过文件扩展名构造 PyFormatOptions,并显式 with_preview(PreviewMode::Enabled)
  4. 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_confdirty_decay_msmuzzy_decay_ms 都设为 -1linter.rs),代码注释解释了原因:默认 10s 的 decay 可能表现为随机性的慢分配,而基准场景并不需要把未分配页面归还给 OS,禁用 decay 后测量更稳定。

ty 相关基准:类型检查器的微基准与真实项目耗时

仓库中 ty(类型检查引擎)处于开发阶段,其性能验证同样收敛到本 crate。这类基准分成两组:

打桩微基准 tyty_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_casesetup_rayon)被这些基准共享。

墙钟耗时 ty_walltimety_script:改用 Divan,直接面向真实项目。其基础设施位于 src/real_world_projects.rs,工作方式如下:

  1. 把目标仓库克隆/更新到 ./target 下的缓存目录(顶层常量 TY_ECOSYSTEM_PIN = "2026-06-17T06:46:32Z" 用于固定依赖时间窗,防止外部依赖漂移引入抖动);
  2. 对带依赖的项目用 uv 创建虚拟环境并安装依赖;
  3. 可选:将整个项目结构复制进内存文件系统,减少基准中的 IO 噪音;
  4. 随后构造 ProjectDatabase 并执行基准。

例如 benches/ty_walltime.rs 中,每次迭代会 ProjectMetadata::discover 发现项目、覆写 Options(指定 Python 版本、.venv 路径等),再以 OsSystem 打开真实文件系统进行全量检查计时。

模块解析基准 module_resolutionbenches/module_resolution.rs 测量的是"随着额外搜索路径(extra_paths)数量增长,冷模块解析查询批次的耗时变化"。它在内存文件系统(TestSystem)中构造从 0 到 N 个 /extra/p{n} 目录,并用三类查询考察解析器行为:预先 seed 的目标模块(命中缓存/逐级查找)、不存在的模块名(练习 stub-overlay 发现后的回退)、标准库模块名(ossystyping 等)。同一批次内的查询共享底层 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 目标名(如 lexerparserformatterty),也可以写更细的分组名(如 linter/default-ruleslinter/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:先以 profiling profile 编译(cargo bench -p ruff_benchmark --no-run --profile=profiling),再用 perf record 录制指定时长的基准执行(示例命令含 --profile-time=1,即每种场景只跑 1 秒,详见 CONTRIBUTING.md 的 Profiling Projects 一节);
  • macOS + cargo-instrumentscargo 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 lexerparserformatterty
精简/快速输出 cargo bench -p ruff_benchmark -- --quiet-- --quick
PR 对比摘要 分别 --save-baseline=main--save-baseline=prcritcmp main pr

进一步阅读与源码入口:

需要强调,这些基准衡量的是改动前后的相对回归,而非对外部工具的绝对结论;本文所有数据与结论均以当前仓库文档与源码为准。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
595
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
918
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.6 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
517
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
389