news 2026/10/12 3:28:04

使用 `[criterion]` 过程宏:Criterion.rs 自定义测试框架实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
使用 `[criterion]` 过程宏:Criterion.rs 自定义测试框架实战指南
  • 开发工具
  • 性能测试

【免费下载链接】criterion.rs

Statistics-driven benchmarking library for Rust

项目地址:https://gitcode.com/gh_mirrors/cr/criterion.rs
点击查看免费下载

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 = false

nightly 版本的 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)))); }

逐行拆解这份配置:

  1. #![feature(custom_test_frameworks)]:在 crate 根部启用 nightly 的自定义测试框架特性;
  2. #![test_runner(criterion::runner)]:声明本 crate 的测试由criterion::runner这个函数来运行(它定义在 src/lib.rs,后面会细讲);
  3. use criterion_macro::criterion;:引入#[criterion]过程宏本体。目前必须从criterion_macro导入;官方文档说明,未来版本很可能将其从criterioncrate 直接 re-export,届时就可以从criterion导入;
  4. #[criterion]:挂在接收&mut Criterion参数的函数上,函数体内部照常使用c.bench_function(...)定义基准;
  5. #[criterion(custom_criterion())]:带参形式。括号内是任意一个返回Criterion对象的表达式,用于覆盖默认配置——示例里custom_criterion将采样数改为 50;
  6. 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]能平滑取代传统宏的原因。

使用注意事项

  1. unstable 特性依赖:#![feature(custom_test_frameworks)]、#[test_case]都是 nightly 专有特性,stable 编译器无法编译此类基准;
  2. API 仍处于实验期:官方文档明确提醒,Criterion.rs 及其测试框架的 API 设计仍在演化中;criterion-macro子 crate 会遵守 SemVer,但未来出现破坏性变更的可能性相当大。这意味着升级依赖版本时需留意 changelog;
  3. 导入路径可能变化:当前#[criterion]必须从criterion_macro导入,未来可能会改为从criterion直接 re-export;
  4. black_box的来源:应使用std::hint::black_box,旧版criterion::black_box()已废弃;
  5. 实验性的验证途径:仓库内 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

项目地址:https://gitcode.com/gh_mirrors/cr/criterion.rs
点击查看免费下载

相关推荐

上一篇:终极免费游戏王离线对战:YgoMaster完整指南
下一篇:如何用PyPortfolioOpt实现智能投资组合优化?从理论到实战的完整指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/12 3:27:51

戴尔PowerStore存储升级实战:容量翻倍、文件服务与灾备体系优化

前两个月我帮某制造企业完成了一次戴尔PowerStore存储阵列的平台级升级&#xff0c;目标很直接&#xff1a;容量翻倍、文件操作能力补强、灾备体系重新梳理。升级之前&#xff0c;这台阵列的处境在不少运维团队里都很典型——虚拟化和数据库平台挤在同一个存储池里&#xff0c;…

作者头像 李华
网站建设 2026/10/12 3:23:01

虚拟电厂日前日内双时间尺度调度:Matlab+Yalmip建模详解

这两年做虚拟电厂&#xff08;VPP&#xff09;优化调度&#xff0c;我前前后后复现过不少公开论文里的模型&#xff0c;最实用、落地价值最高的仍然是“日前调度 日内调度”这套双时间尺度框架。网上流传的版本很多&#xff0c;但真正把两层逻辑讲清楚、代码能直接跑通的却不多…

作者头像 李华
网站建设 2026/10/12 3:22:52

remocn一次性讲透27种Remotion场景转场:从硬切到Shader擦除

【免费下载链接】remocn Production-ready animations, transitions, backgrounds, and scenes for Remotion 项目地址&#xff1a; https://gitcode.com/gh_mirrors/re/remocn 点击查看 免费下载 remocn 是一个面向 Remotion 的复制粘贴式动效组件库&#xff0c;它的 Transit…

作者头像 李华
网站建设 2026/10/12 3:20:46

mysql获取分组中的指定数据(附四大排序函数说明)

目录一.背景二.解决方案1.先排序后分组方式2.利用rank() over...&#xff08;推荐&#xff09;3 mysql四大排名函数&#xff08;1&#xff09;排序条件下的排名&#xff08;2&#xff09;分区排序条件下的排名一.背景 &#xff1a; 举个例子&#xff0c;现有两张表分别是老师和…

作者头像 李华