Mau
一个强大的 Rust 过程宏库,提供记忆化(memoization)功能和高效的范围操作宏。
功能特性
- ✅ 自动记忆化:
#[memo]属性宏,智能缓存管理 - ✅ 智能清理:
solve!宏,自动清空缓存,避免内存泄漏 - ✅ 生命周期控制:
lifetime参数,精确控制缓存保留策略 - ✅ 智能缓存键: 三种键模式(
ptr、ref、val),平衡性能和功能 - ✅ 线程模式: 单线程(
single)和多线程(multi)支持 - ✅ 范围宏:
min!、max!、sum!、and!、or!、reduce!、fold!等高效宏 - ✅ 灵活语法: 支持多参数、数组、范围等多种调用方式
- ✅ 空迭代器处理:
min!和max!对空迭代器返回边界值
安装
[]
= "0.1.16"
快速开始
1. 基础记忆化
use memo;
性能提升:
- 不使用 memo:~1 秒
- 使用 memo:~0.01 毫秒
- 性能提升:100,000 倍!
2. 智能清理缓存
use ;
3. 范围宏
use ;
核心功能详解
#[memo] - 自动记忆化
为函数添加记忆化,自动缓存计算结果:
生成的辅助函数:
// 自动生成:
// - fibonacci_start(n) : 调用并清空缓存
// - fibonacci_clear() : 手动清空缓存
solve! 宏 - 智能清理
自动清空缓存,避免内存泄漏:
use ;
何时使用 solve!:
✅ 应该使用:
- 单次调用中有大量递归(如动态规划)
- 参数范围很大,不需要跨调用缓存
- 需要控制内存使用
❌ 不应该使用:
- 需要长期保留缓存(跨多次调用)
- 参数经常重复,缓存命中率高
手动清理缓存
参数配置
线程模式(thread):
single(默认):单线程,性能最佳multi:多线程安全,全局共享
键模式(key):
ptr(默认):地址+长度,最快ref:先比地址+长度,再比内容,平衡性能val:深度比较,功能最完整
生命周期模式(lifetime):
problem(默认):每次_start()调用后清除缓存program:保留缓存直到程序结束(仅在键不包含地址时有效)
使用语法
// 使用默认配置
// 命名参数(推荐)
// 长期保留缓存(需要 key=val)
键模式详解
ptr 模式 - 最快,地址+长度
// 示例:
let arr = vec!;
process; // 第1次:计算
process; // 第2次:命中 ✓(相同地址+长度)
let arr2 = vec!; // 内容相同,地址不同
process; // 第3次:重新计算(地址不同)
// 切片长度不同
process; // 第4次:重新计算(长度不同)
缓存键:(地址, 长度)
何时使用:相同引用会反复调用(如递归中传递同一个数组)
ref 模式,先比地址+长度,再比内容
// 或 #[memo]
// 示例:
let arr = vec!;
process; // 第1次:计算
process; // 第2次:命中 ✓(地址+长度相等)
let arr2 = vec!; // 内容相同,地址不同
process; // 第3次:命中 ✓(地址不等,但内容相等)
let arr3 = vec!; // 内容不同
process; // 第4次:重新计算(内容不等)
// 切片长度不同
process; // 第5次:重新计算(长度不同)
工作原理:
- 快速路径:比较
(地址, 长度),相等则命中 - 慢速路径:地址或长度不等时,比较内容
何时使用:大部分情况的最佳选择
val 模式 - 功能最完整,深度比较
何时使用:复杂嵌套类型,需要深度比较
三种模式对比
| 模式 | 比较方式 | 相同地址+长度 | 不同地址+相同内容 | 性能 | 适用场景 |
|---|---|---|---|---|---|
ptr |
地址+长度 | ⚡极快 | ❌不命中 | 最快 | 一般情况(默认) |
ref |
先比地址+长度,若相等则命中;否则比内容 | ⚡快 | ✅命中 | 快 | 内容可能重复 |
val |
深度比较 | 慢 | ✅命中 | 慢 | 复杂嵌套类型 |
生命周期模式详解
lifetime 参数控制缓存何时被清除,这对内存管理至关重要。
problem 模式(默认)- 问题级别缓存
// 或 #[memo]
何时使用:
- ✅ 单次问题求解(如 OJ 题目、一次性计算)
- ✅ 需要控制内存使用
- ✅ 参数范围很大,不需要跨问题复用
program 模式 - 程序级别缓存
何时使用:
- ✅ 配置解析、数据计算等需要长期复用的结果
- ✅ 多次请求/调用相同参数值
- ✅ 缓存命中率高,相同输入会在不同问题中重复出现
重要:program 模式的生效条件
lifetime=program 只有在缓存键不包含地址信息时才会保留缓存。
| 函数参数类型 | key 模式 | lifetime=program | 是否保留缓存 | 原因 |
|---|---|---|---|---|
有引用参数 (如 &[i32]) |
ptr |
❌ 清除 | 否 | 键包含地址,旧地址无法复用,浪费内存 |
有引用参数 (如 &[i32]) |
ref |
❌ 清除 | 否 | 键包含地址,旧地址无法复用 |
有引用参数 (如 &[i32]) |
val |
✅ 保留 | 是 | 键只基于值,可跨问题复用 |
无引用参数 (如 i32) |
ptr |
✅ 保留 | 是 | 无引用,键完全基于值 |
无引用参数 (如 i32) |
ref |
✅ 保留 | 是 | 无引用,键完全基于值 |
无引用参数 (如 i32) |
val |
✅ 保留 | 是 | 键完全基于值 |
示例说明:
// ✅ 会保留缓存:无引用参数,键完全基于值
// ✅ 会保留缓存:有引用参数 + key=val,键基于值
// ❌ 不会保留缓存:有引用参数 + key=ptr,键包含地址
// ❌ 不会保留缓存:有引用参数 + key=ref,键包含地址
为什么这样设计?
情况1:键包含地址(ptr/ref 模式 + 引用参数)
- 每个问题的数组地址不同,旧缓存无法被新问题访问
- 保留缓存只会浪费内存,因此即使设置
program也会清除
情况2:键不包含地址(val 模式或无引用参数)
- 键完全基于值,相同输入可以跨问题复用
- 保留缓存可以提高性能
使用场景
场景 1: 动态规划(使用 start! 自动清理)
use ;
场景 2: Web 服务(长期缓存)
场景 3: 互相递归
use ;
范围宏
高效的范围聚合操作,支持多种灵活的调用语法。
多种语法支持
use ;
fold! 宏 - 自定义累积操作
fold! 提供了最灵活的累积操作:
use fold;
use HashMap;
fold! vs reduce!:
| 特性 | fold! | reduce! |
|---|---|---|
| 初始值 | 需要提供 | 使用第一个元素 |
| 空序列 | 返回初始值 | panic |
| 累加器类型 | 可与元素类型不同 | 必须相同 |
| 灵活性 | 高 | 中 |
use ;
let data = vec!;
// reduce: 使用第一个元素作为初始值
let sum1 = reduce!;
// 相当于: 1 + 2 + 3 + 4 + 5 = 15
// fold: 提供初始值
let sum2 = fold!;
// 相当于: 0 + 1 + 2 + 3 + 4 + 5 = 15
// fold 的优势:可以处理空序列
let empty: = vec!;
let result = fold!;
println!; // 100(返回初始值)
// reduce 会 panic
// let result = reduce!(|i| empty[i], 0..0, |a, b| a + b); // panic!
空迭代器处理
let empty: = vec!;
// min! 返回类型的 MAX 值
println!; // i32::MAX = 2147483647
// max! 返回类型的 MIN 值
println!; // i32::MIN = -2147483648
// sum! 返回 0(加法的单位元)
let empty_sum: = vec!;
println!; // 0
// 浮点数也返回 0.0
let empty_f64: = vec!;
println!; // 0.0
// 不支持的类型会 panic(仅针对 min/max)
let empty_str: = vec!;
// min!(empty_str); // panic: "type does not have a MAX value"
支持的类型:
- ✅ 整数:
i8i128、u8u128、isize、usize - ✅ 浮点:
f32、f64 - ✅ 字符:
char(仅 min/max) - ❌ 字符串等:运行时 panic(仅 min/max,sum 不支持字符串)
详细示例
动态规划:背包问题
use ;
多参数记忆化
use memo;
范围宏高级用法
自定义归约
use reduce;
短路优化
use ;
性能数据
记忆化性能提升
| 算法 | 规模 | 不使用 memo | 使用 memo | 提升倍数 |
|---|---|---|---|---|
| Fibonacci | n=30 | 10 ms | 0.01 ms | 1,000x |
| Fibonacci | n=40 | 1000 ms | 0.01 ms | 100,000x |
| Fibonacci | n=50 | >60秒 | 0.01 ms | >6,000,000x |
| LCS | 长度50 | 10秒 | 0.1秒 | 100x |
| 背包问题 | 50项 | 5秒 | 0.05秒 | 100x |
键模式性能对比
测试:10,000 次调用,缓存已预热
| 模式 | 时间 | 相对性能 |
|---|---|---|
ptr |
1.2 ms | 100% |
ref |
1.5 ms | 80% |
val |
3.4 ms | 35% |
使用建议
何时使用记忆化
✅ 应该使用:
- 递归函数有重复子问题
- 动态规划算法
- 计算代价高但参数经常重复
- 纯函数(无副作用)
❌ 不应该使用:
- 函数有副作用(I/O、打印等)
- 参数几乎不重复
- 计算非常简单
键模式选择策略
// 场景1:递归中传递同一个引用
// 场景2:不同调用但参数可能相同(内容可能重复)
// 场景3:复杂嵌套类型
注意事项
1. 避免副作用
// ❌ 错误:有副作用
// ✅ 正确:纯函数
2. 参数设计
// ❌ 错误:无关参数导致缓存失效
// ✅ 正确:只包含必要参数
3. 内存监控
4. f64 类型处理
// ❌ f64 不实现 Hash 和 Eq
// #[memo]
// fn calc(x: f64) -> f64 { x * x } // 编译错误
// ✅ 使用引用(自动转换为 u64)
// ✅ 或使用 val 模式
参数速查表
#[memo] 参数
// 默认:thread=single, key=ptr, lifetime=problem
// 命名参数
// 多线程 + 地址键
// 只指定 key
// 长期保留缓存(需要 key=val)
// 完整指定
solve! 宏语法
solve! // 单个函数调用
solve! // 多个调用(元组)
solve! // 代码块
solve! // 嵌套调用(自动递归替换)
范围宏语法
// min, max, sum, and, or
min! // 两参数(新增支持)
min! // 多参数
min! // 整个数组
min! // 范围表达式
min! // 包含范围(闭区间)
min! // 迭代器
// reduce - 自定义归约
reduce!
// fold - 带初始值的累积
fold!
常见问题
Q1: ref 模式比 ptr 慢多少?
A: 在缓存已预热的情况下,ref 模式约为 ptr 模式的 80% 性能。但 ref 模式功能更强(内容相同就命中),是大多数情况的最佳选择。
Q2: 为什么 ref 模式需要比较地址和长度?
A: 避免相同地址不同长度的错误命中:
let data = vec!;
&data // addr = data.as_ptr(), len = 2
&data // addr = data.as_ptr(), len = 5 ← 地址相同!
// 如果只比地址,会错误地认为这两个切片相同
// 同时比较地址和长度后:(addr1, len1) != (addr2, len2)
Q3: 空迭代器为什么返回边界值?
A: 符合数学定义:
min(空集) = +∞→ 返回MAXmax(空集) = -∞→ 返回MIN
这样可以避免 panic,提供更好的默认行为。
Q4: 如何处理 f64 类型?
A: 使用引用参数,宏会自动转换:
Q5: 为什么我设置了 lifetime=program 但缓存还是被清除?
A: lifetime=program 只在键中不包含地址信息时才生效。检查:
-
函数是否有引用参数且使用了
key=ptr或key=ref?// ❌ 有引用参数 + ptr/ref,键包含地址,program 无效 // ✅ 无引用参数,键基于值,program 有效 -
是否使用了
key=val?// ✅ val 模式键只基于值,program 有效 // ❌ ptr/ref 模式键包含地址,program 无效
总结:只有 (无引用参数) OR (有引用参数 AND key=val) 时,lifetime=program 才会保留缓存。
Q6: min!(1, 2) 两参数语法何时可用?
A: v0.1.12 及以上版本支持。如果遇到错误,请升级:
[]
= "0.1.16" # 或更高版本
完整示例
use ;
// 长期缓存:配置解析
// 临时缓存:动态规划
最佳实践总结
记忆化配置
- 默认使用
key=ptr:性能最佳,适合大多数场景 - 内容可能重复用
key=ref:兼顾性能和功能 - 复杂嵌套类型用
key=val:功能最完整
生命周期选择
- 单次计算用
lifetime=problem(默认):自动清理,避免内存泄漏 - 长期缓存用
lifetime=program:跨调用保留,需要满足条件:- ✅ 无引用参数(键基于值)
- ✅ 或者:有引用参数 + 使用
key=val(键基于值) - ❌ 不能用
key=ptr或key=ref(键包含地址,会自动清除)
使用技巧
- 单次问题求解用
solve!:自动清理,推荐用于 OJ、算法竞赛 - Web 服务、配置解析等用
lifetime=program:长期复用 - 递归传递相同引用用
ptr:最快 - 避免副作用:只在纯函数上使用
- 监控内存:参数范围大时使用
solve!或手动_clear()
范围宏选择
- 简单聚合用
min!/max!/sum!:最简洁 - 自定义操作用
reduce!:灵活 - 需要初始值用
fold!:最强大,可处理空序列和类型转换 - 遍历执行用
each!:对每个元素执行操作
each! 宏
each! 宏用于遍历范围内的每个索引并执行闭包,等价于 for 循环:
use each;
let data = vec!;
let mut results = Vecnew;
// 等价于 for i in 0..data.len() { results.push(data[i]); }
each!;
// 输出: [3, 1, 4, 1, 5, 9]
println!;
支持的语法:
- 范围表达式:
each!(|i| { ... }, 0..data.len()) - 闭区间:
each!(|i| { ... }, 0..=5) - 部分范围:
each!(|i| { ... }, 2..5) - 任何迭代器:
each!(|x| { ... }, data.iter())
更新日志
v0.1.16 (最新)
- ✨ 新增
each!宏:对指定范围内的每个索引执行闭包- 语法:
each!(|i| { statements }, 0..data.len()) - 等价于
for i in 0..data.len() { statements } - 支持范围表达式和迭代器
- 语法:
- 🔧 ptr/ref 模式智能缓存清除:使用
ptr或ref模式时,通过solve!调用会自动清除缓存- 避免内存泄漏:地址模式下的缓存无法跨问题复用
- 提升性能:减少无效缓存占用内存
- 📝 更新文档:添加
each!宏和缓存清除策略说明
v0.1.14
- 🐛 修复
lifetime=program逻辑:修正了缓存保留/清除的判断条件- 正确行为:键不包含地址时保留缓存,键包含地址时清除缓存
- 无引用参数 或
key=val:保留缓存 ✓ - 有引用参数 + (
key=ptr或key=ref):清除缓存 ✓(避免内存浪费)
- 📝 更新文档:同步修正所有关于
lifetime参数的说明和示例
v0.1.13
- ✅ 修复两参数语法:
min!(1, 2)现在可以正常工作 - ✅ 新增
lifetime参数:精确控制缓存生命周期lifetime=problem(默认):调用后清除缓存lifetime=program:保留缓存(仅在键不包含地址时有效)
- ✅ 智能清除逻辑:
lifetime=program会自动检测键是否包含地址信息 - ✅ 新增 50+ 综合测试:fold、lifetime、两参数语法全面测试
v0.1.11
- ✅
solve!宏:自动清理缓存,避免内存泄漏(原名start!) - ✅ 默认键模式改为
ptr(性能最佳) - ✅
ref模式添加长度比较,修复切片缓存错误
v0.1.10
- ✅ 三层记忆化结构,智能缓存管理
- ✅ 生成
_start()和_clear()辅助函数
v0.1.8
- ✅
key=ref可以直接使用,不需要r#ref - ✅ 参数验证:无效的参数名或模式会在编译时报错
v0.1.7
- ✅
ref模式:先比地址,若相等则命中;否则再比内容 - ✅ 参数重命名:
thread_mode→thread,index_mode→key - ✅ 键模式重命名:
light→ptr,normal→ref,heavy→val - ✅ 线程模式重命名:
local→single - ✅
ptr模式改进:使用 (地址, 长度) 作为键 - ✅
min!/max!空迭代器返回边界值
许可证
MIT 或 Apache-2.0 双许可证。