- 开发工具
- 性能测试
【免费下载链接】criterion.rs
Statistics-driven benchmarking library for Rust
criterion-macro是 Criterion.rs 仓库中的一个独立子 crate,它提供名为#[criterion]的过程宏,让你可以用属性标记代替传统的criterion_group!/criterion_main!宏来定义基准测试。本文将以该 crate 及其配套代码为骨架,讲解这一实验性自定义测试框架的启用方式、完整配置示例、宏展开的底层原理,并对比传统写法的差异,帮助你在 nightly Rust 上提前体验 Criterion.rs 未来的标准基准定义方式。
为什么需要#[criterion]:从传统宏到属性标记
在 Criterion.rs 的传统用法中,定义一个基准需要两步:先用criterion_group!把多个基准函数聚合为一个"组",再用criterion_main!生成一个包含所有组的main函数。同时,由于要接管基准测试的入口,你必须在Cargo.toml中为对应[[bench]]目标显式关闭默认测试框架:
[[bench]] name = "my_bench" harness = falsenightly 版本的 Rust 编译器支持自定义测试框架(custom test frameworks)。Criterion.rs 基于此提供了一个实验性实现:你可以直接给基准函数挂上#[criterion]属性,由编译器和criterion::runner来统一调度,不再需要criterion_group!/criterion_main!,也不需要为#[criterion]基准关闭默认测试框架(harness = false仅适用于传统criterion_group!/criterion_main!写法)。
官方文档(见 book/src/user_guide/custom_test_framework.md)指出:未来某个时间点criterion_group!/criterion_main!将被弃用,#[criterion]会成为定义 Criterion.rs 基准的标准方式。目前这一能力依赖若干 unstable 特性,属于"提前尝鲜"阶段。
crate 结构与定位:criterion-macro是什么
在仓库中,criterion-macro是一个独立的 package(目录 macro/),其 macro/Cargo.toml 中关键配置如下:
[package] name = "criterion-macro" version = "0.4.0" edition = "2021" description = "Custom Test Framework macro for Criterion.rs" [lib] proc-macro = true [dependencies] proc-macro2 = { version = "1.0", features = ["nightly"] } quote = "1.0" [dev-dependencies] criterion = { version = "0.4.0", path = "..", default-features = false } [[bench]] name = "test_macro_bench"几点值得注意:
[lib] proc-macro = true表明这是一个纯粹的过程宏 crate,编译产物是动态库形式的过程宏,供其他 crate 引用;- 它只依赖
proc-macro2和quote两个轻量库,自身不包含任何基准测试运行逻辑——真正的运行逻辑在criterion主 crate 中,宏只负责"改写代码"; - dev-dependencies 通过
path = ".."引用仓库根目录的criterion主 crate,用于编译并测试宏展开后的代码; - 仓库当前工作区的
criterion主 crate 版本为 0.7.0(见 Cargo.toml),criterion-macro的版本号独立演进。
宏本身定义在 macro/src/lib.rs,整个文件只有一个过程宏入口函数criterion,是名副其实的"小而专"。
环境准备:启用自定义测试框架
由于自定义测试框架仍是 unstable 特性,你必须使用较新的 nightly 编译器。然后向Cargo.toml的[dev-dependencies]中添加两个依赖:
[dev-dependencies] criterion = "0.5" criterion-macro = "0.4"说明:以上版本号取自官方用户指南(book/src/user_guide/custom_test_framework.md)中的示例;本仓库中criterion-macro当前版本为 0.4.0(macro/Cargo.toml),主 crate 当前版本为 0.7.0(Cargo.toml),实际使用时请以 crates.io 上可用的版本为准,并确保二者相互兼容。
与criterion_group!/criterion_main!写法不同,使用#[criterion]时不需要设置harness = false,因为此时基准函数就是普通测试函数,由自定义测试框架统一调度。
完整示例:从 Fibonacci 到自定义 Criterion 对象
下面的完整示例与仓库中的真实示例文件 macro/benches/test_macro_bench.rs 一致(该文件同样出现在官方用户指南中),假设你使用 Rust 2018 及以上版本:
#![feature(custom_test_frameworks)] #![test_runner(criterion::runner)] use std::hint::black_box; use criterion::Criterion; use criterion_macro::criterion; fn fibonacci(n: u64) -> u64 { match n { 0 | 1 => 1, n => fibonacci(n - 1) + fibonacci(n - 2), } } fn custom_criterion() -> Criterion { Criterion::default() .sample_size(50) } #[criterion] fn bench_simple(c: &mut Criterion) { c.bench_function("Fibonacci-Simple", |b| b.iter(|| fibonacci(black_box(10)))); } #[criterion(custom_criterion())] fn bench_custom(c: &mut Criterion) { c.bench_function("Fibonacci-Custom", |b| b.iter(|| fibonacci(black_box(20)))); }逐行拆解这份配置:
#![feature(custom_test_frameworks)]:在 crate 根部启用 nightly 的自定义测试框架特性;#![test_runner(criterion::runner)]:声明本 crate 的测试由criterion::runner这个函数来运行(它定义在 src/lib.rs,后面会细讲);use criterion_macro::criterion;:引入#[criterion]过程宏本体。目前必须从criterion_macro导入;官方文档说明,未来版本很可能将其从criterioncrate 直接 re-export,届时就可以从criterion导入;#[criterion]:挂在接收&mut Criterion参数的函数上,函数体内部照常使用c.bench_function(...)定义基准;#[criterion(custom_criterion())]:带参形式。括号内是任意一个返回Criterion对象的表达式,用于覆盖默认配置——示例里custom_criterion将采样数改为 50;black_box:来自std::hint::black_box(而非旧版的criterion::black_box,CHANGELOG.md 明确要求迁移),防止编译器把看似无用的计算优化掉。
宏展开原理:#[criterion]背后发生了什么
#[criterion]是一个属性过程宏(#[proc_macro_attribute]),定义在 macro/src/lib.rs。它接收两部分输入:属性参数(attr)和被标注的函数项(item),最终输出一个包装函数。核心逻辑如下:
#[proc_macro_attribute] pub fn criterion(attr: TokenStream, item: TokenStream) -> TokenStream { let span = proc_macro2::Span::call_site(); let init = if stream_length(attr.clone()) != 0 { attr } else { quote_spanned!(span=> criterion::Criterion::default()) }; let function_name = find_name(item.clone()); let wrapped_name = Ident::new(&format!("criterion_wrapped_{}", function_name.to_string()), span); let output = quote_spanned!(span=> #[test_case] pub fn #wrapped_name() { #item let mut c = #init.configure_from_args(); #function_name(&mut c); } ); output.into() }展开过程可以拆成几个关键步骤:
init分支判断:通过stream_length(attr)统计属性参数流中的 token 数量。若#[criterion]不带参数,则默认使用criterion::Criterion::default();若带参数(如#[criterion(custom_criterion())]),则原样保留该表达式。这对应了"自定义 Criterion 对象"的两种形态;find_name提取函数名:宏遍历被标注函数体的 token 流,找到fn关键字后的第一个标识符作为基准函数名(macro/src/lib.rs),找不到就panic!("Unable to find function name");- 生成包装函数:宏把原始函数
#item原封不动嵌入,然后生成一个名为criterion_wrapped_<原函数名>的pub fn。包装函数体内:先创建Criterion实例(#init),调用.configure_from_args()让命令行参数生效,再把&mut c传给原基准函数; #[test_case]标记:#[test_case]是 nightly 自定义测试框架中标记"测试用例"的属性,编译器收集所有带此标记的顶层函数,生成一个数组交给#![test_runner(...)]指定的 runner 执行。
可以看到:宏本身不改变基准逻辑,它的职责是"把带属性的函数包装成可被自定义测试框架识别的测试用例,并在运行前统一完成Criterion的初始化与命令行配置"。
runner 如何工作:统一调度与最终汇总
#[test_runner(criterion::runner)]声明的 runner 实现在 src/lib.rs:
#[doc(hidden)] pub fn runner(benches: &[&dyn Fn()]) { for bench in benches { bench(); } Criterion::default().configure_from_args().final_summary(); }- runner 接收编译器收集到的所有
#[test_case]函数的引用数组; - 逐个调用它们(每个包装函数内部会执行对应的
#[criterion]基准); - 全部执行完毕后,创建一个默认
Criterion实例,调用.configure_from_args()解析命令行参数,再调用.final_summary()输出整体汇总报告。final_summary与configure_from_args的签名可分别在 src/lib.rs 与 src/lib.rs 查到。
值得注意的是,CHANGELOG.md 中有一条修复记录:"Fixed#[criterion]benchmarks ignoring the command-line options"——即早期版本中#[criterion]基准会忽略命令行选项,后来通过包装函数里的configure_from_args()调用修复。这从侧面印证了"命令行参数统一在宏展开层处理"这一设计。
与传统criterion_group!/criterion_main!的对比
传统写法的核心宏定义在 src/macros.rs:
criterion_group! { name = benches; config = Criterion::default(); targets = bench_method1, bench_method2 } criterion_main!(benches);criterion_group!展开为普通函数:内部创建Criterion实例(默认是Criterion::default(),也可通过config = ...传入自定义表达式)、调用.configure_from_args()后依次调用各个目标基准函数;criterion_main!则展开为一个main函数,遍历所有组后执行final_summary()。
对比两种方式:
| 维度 | criterion_group!/criterion_main! | #[criterion](criterion-macro) |
|---|---|---|
| 编译器要求 | stable 可用 | 需要 nightly(custom_test_frameworks特性) |
| 定义基准 | 组 + 入口函数两步声明 | 属性标记一步完成 |
| harness | 需要harness = false | 无需关闭默认测试框架 |
| 自定义配置 | config = 表达式参数 | #[criterion(表达式)]属性参数 |
| 状态 | 当前标准方式 | 实验性,未来将成为标准 |
在 API 设计思路上,两者高度同构:config参数对应属性括号内的表达式,configure_from_args()和final_summary()的调用时机也一一对应——这正是#[criterion]能平滑取代传统宏的原因。
使用注意事项
- unstable 特性依赖:
#![feature(custom_test_frameworks)]、#[test_case]都是 nightly 专有特性,stable 编译器无法编译此类基准; - API 仍处于实验期:官方文档明确提醒,Criterion.rs 及其测试框架的 API 设计仍在演化中;
criterion-macro子 crate 会遵守 SemVer,但未来出现破坏性变更的可能性相当大。这意味着升级依赖版本时需留意 changelog; - 导入路径可能变化:当前
#[criterion]必须从criterion_macro导入,未来可能会改为从criterion直接 re-export; black_box的来源:应使用std::hint::black_box,旧版criterion::black_box()已废弃;- 实验性的验证途径:仓库内 macro/benches/test_macro_bench.rs 是可直接运行的官方示例,其
[[bench]] name = "test_macro_bench"声明见 macro/Cargo.toml。在 nightly 环境下用cargo bench即可观察两个基准(Fibonacci-Simple与Fibonacci-Custom)的执行与结果输出。
延伸阅读
- criterion-macro 源码:
#[criterion]属性宏的完整实现(约 56 行); - criterion-macro 示例基准:可直接运行的完整示例;
- 自定义测试框架官方指南:本文内容对应的官方文档原文;
- 传统宏实现源码:
criterion_group!/criterion_main!的展开定义; - runner 实现:
criterion::runner的调度与汇总逻辑; - criterion-macro 的 LICENSE-APACHE 与 LICENSE-MIT:该项目采用 Apache-2.0 / MIT 双许可,参与贡献请参阅 macro/CONTRIBUTING.md。
- 开发工具
- 性能测试
【免费下载链接】criterion.rs
Statistics-driven benchmarking library for Rust
相关推荐
Criterion.rs 自定义测试框架:使用 `[criterion]` 属性替代传统宏定义基准测试
Criterion.rs 自定义测试框架:使用 criterion 属性替代传统宏定义基准测试 导读 criterion 是 Criterion.rs 提供的实
开发工具性能测试3步打造专属AI歌手:Retrieval-based-Voice-Conversion-WebUI语音克隆终极指南
3步打造专属AI歌手:Retrieval based Voice Conversion WebUI语音克隆终极指南 你是否曾梦想过拥有自己的AI歌手?或者将普通
人工智能AI 应用语音音频深度学习cargo-criterion 实战指南:用 Cargo 扩展接管 Criterion.rs 基准测试的统计分析与报告生成
cargo criterion 实战指南:用 Cargo 扩展接管 Criterion.rs 基准测试的统计分析与报告生成 cargo criterion 是
开发工具性能测试
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考