Mau
一个强大的 Rust 过程宏库,提供记忆化(memoization)功能和高效的范围操作宏。
功能特性
- ✅ 自动记忆化:
#[memo]属性宏,为单个函数添加缓存 - ✅ 批量记忆化:
memo_block!函数宏,智能缓存管理,避免内存泄漏 - ✅ 智能缓存键: 三种键模式(
ptr、ref、val),平衡性能和功能 - ✅ 线程模式: 单线程(
single)和多线程(multi)支持 - ✅ 范围宏:
min!、max!、sum!、and!、or!、reduce!等高效宏 - ✅ 空迭代器处理:
min!和max!对空迭代器返回边界值
安装
[]
= "0.1.7"
快速开始
1. 基础记忆化
use memo;
性能提升:
- 不使用 memo:~1 秒
- 使用 memo:~0.01 毫秒
- 性能提升:100,000 倍!
2. 互相递归(批量记忆化)
use memo_block;
memo_block!
3. 范围宏
use ;
核心功能详解
#[memo] - 长期缓存
为单个函数添加记忆化,缓存会永久保留直到程序结束。
参数配置
线程模式(thread):
single(默认):单线程,性能最佳multi:多线程安全,全局共享
键模式(key):
ptr:只比较地址,最快ref(默认):先比地址再比内容,平衡性能和功能val:深度比较,功能最完整
使用语法
// 使用默认配置
// 命名参数(推荐)
// 注意:ref 是关键字,需要写成 r#ref
键模式详解
ptr 模式 - 最快,只比地址
// 示例:
let arr = vec!;
process; // 第1次:计算
process; // 第2次:命中 ✓(相同地址)
let arr2 = vec!; // 内容相同,地址不同
process; // 第3次:重新计算(地址不同)
何时使用:相同引用会反复调用(如递归中传递同一个数组)
ref 模式 - 默认,先比地址再比内容
// 或 #[memo]
// 示例:
let arr = vec!;
process; // 第1次:计算
process; // 第2次:命中 ✓(相同地址,极快)
let arr2 = vec!; // 内容相同,地址不同
process; // 第3次:命中 ✓(地址不同,比较内容)
let arr3 = vec!; // 内容不同
process; // 第4次:重新计算(内容不同)
工作原理:
- 相同地址 → 立即返回(最快)
- 不同地址 → 比较内容,相同就命中
何时使用:大部分情况的最佳选择
val 模式 - 功能最完整,深度比较
何时使用:复杂嵌套类型,需要深度比较
三种模式对比
| 模式 | 比较方式 | 相同地址 | 不同地址+相同内容 | 性能 | 适用场景 |
|---|---|---|---|---|---|
ptr |
地址+长度 | ⚡极快 | ❌不命中 | 最快 | 相同引用反复调用 |
ref |
地址→内容 | ⚡快 | ✅命中 | 快 | 一般情况(推荐) |
val |
深度比较 | 慢 | ✅命中 | 慢 | 复杂嵌套类型 |
memo_block! - 智能缓存管理
memo_block! 解决了 #[memo] 的两个核心问题:
问题 1:缓存永不清空导致内存泄漏
// 使用 #[memo]
问题 2:手动清空会破坏递归中的缓存
假设我们尝试在 #[memo] 上手动清空缓存:
// ❌ 错误的做法
// 问题:
fib
└─ fib
└─ fib !
└─ fib ,重新计算 ❌
后果:递归调用中,内层调用清空缓存后,外层调用无法使用缓存,失去了记忆化的意义。
解决方案:memo_block! 的智能清理
memo_block! 实现了智能的自动清理机制:
memo_block!
// 工作原理:
fib // 最外层调用
├─ fib // 内层调用,缓存保留 ✓
│ ├─ fib // 缓存保留 ✓
│ │ └─ ...
│ └─ fib // 缓存命中 ✓
└─ fib // 缓存命中 ✓
// 最外层调用结束 -> 自动清空缓存 ✓
// 特点:
// - 递归过程中:缓存正常工作
// - 调用结束后:自动清空,释放内存
// - 调用次数:11 次(vs 不使用缓存的 177 次)
何时使用 memo_block!
✅ 应该使用 memo_block!:
- 单次调用中有大量递归(如动态规划)
- 参数范围很大,不需要跨调用缓存
- 需要控制内存使用
- 多个函数互相递归
❌ 不应该使用 memo_block!:
- 需要长期保留缓存(跨多次调用)
- 参数经常重复,缓存命中率高
为每个函数单独配置
memo_block! 中的每个函数都可以有自己的配置:
use memo_block;
memo_block!
// 语法规则:
// - 使用 #[memo(...)] 标记
// - 多个参数用逗号分隔:#[memo(thread=multi, key=ptr)]
// - ref 是关键字,需要写成 r#ref
// - 不加属性则使用默认配置
基本使用示例
use memo_block;
memo_block!
#[memo] vs memo_block! 对比
| 特性 | #[memo] |
memo_block! |
|---|---|---|
| 语法 | #[memo] fn f() {} |
memo_block! { fn f() {} } |
| 缓存策略 | 永久保留 | 调用后自动清空 |
| 内存占用 | 持续增长 | 调用后释放 |
| 适用场景 | 参数经常重复,需要长期缓存 | 单次调用优化,控制内存 |
| 互相递归 | 需要分别标记每个函数 | 自动处理所有函数 |
| 性能 | 第2次调用极快(缓存命中) | 每次调用都重新计算 |
| 内存管理 | 手动管理(或不管理) | 自动清理 |
实际场景对比
场景 1:Web 服务器(需要长期缓存)
// ✅ 使用 #[memo]
// 原因:
// - 相同 user_id 会被多次查询
// - 缓存可以避免重复的数据库访问
// - 内存占用可控(用户数量有限)
场景 2:动态规划算法(单次计算优化)
// ✅ 使用 memo_block!
memo_block!
// 原因:
// - 每个问题实例只计算一次
// - 不需要跨调用保留缓存
// - 避免内存泄漏(图可能很大)
场景 3:互相递归(memo_block! 的优势)
// ❌ 使用 #[memo] 的问题
// 问题:需要分别标记,且缓存永不清空
// ✅ 使用 memo_block!
memo_block!
// 优势:
// - 一次性定义所有函数
// - 自动处理互相调用
// - 智能清理,避免内存泄漏
范围宏
高效的范围聚合操作。
基本用法
use ;
空迭代器处理
let empty: = vec!;
// min! 返回类型的 MAX 值
println!; // i32::MAX = 2147483647
// max! 返回类型的 MIN 值
println!; // i32::MIN = -2147483648
// 不支持的类型会 panic
let empty_str: = vec!;
// min!(empty_str); // panic: "type does not have a MAX value"
支持的类型:
- ✅ 整数:
i8i128、u8u128、isize、usize - ✅ 浮点:
f32、f64 - ✅ 字符:
char - ❌ 字符串等:运行时 panic
详细示例
动态规划:背包问题
use memo_block;
memo_block!
多参数记忆化
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:复杂嵌套类型
#[memo] vs memo_block! 选择
使用 #[memo] 的场景:
// 配置计算:参数有限,会重复调用
// 数据转换:同样的输入会多次出现
使用 memo_block! 的场景:
// 动态规划:参数范围大,单次优化
memo_block!
// 递归算法:需要内存控制
memo_block!
注意事项
1. 避免副作用
// ❌ 错误:有副作用
// ✅ 正确:纯函数
2. 参数设计
// ❌ 错误:无关参数导致缓存失效
// ✅ 正确:只包含必要参数
3. 内存监控
// ⚠️ 如果参数范围很大,使用 memo_block!
memo_block!
// 而不是 #[memo](会一直占用内存)
4. f64 类型处理
// ❌ f64 不实现 Hash 和 Eq
// #[memo]
// fn calc(x: f64) -> f64 { x * x } // 编译错误
// ✅ 使用引用(自动转换为 u64)
// ✅ 或使用 val 模式
完整示例:组合使用
use ;
// 长期缓存:配置解析
// 临时缓存:动态规划
memo_block!
参数速查表
#[memo] 参数
// 默认:thread=single, key=ref
// 命名参数
// 多线程 + 地址键
// 只指定 key
memo_block! 参数
memo_block!
范围宏语法
min! // 多参数
min! // 整个数组
min! // 范围表达式
min! // 包含范围
reduce! // 自定义归约
常见问题
Q1: 为什么需要 memo_block!?
A: 解决两个核心问题:
- 内存泄漏:
#[memo]缓存永不清空 - 清理时机:简单清空会破坏递归中的缓存
memo_block! 通过深度跟踪机制,在最外层调用结束后清空缓存,保证递归过程中缓存正常工作。
Q2: ref 模式比 ptr 慢多少?
A: 在缓存已预热的情况下,ref 模式约为 ptr 模式的 80% 性能。但 ref 模式功能更强(内容相同就命中),是大多数情况的最佳选择。
Q3: 空迭代器为什么返回边界值?
A: 符合数学定义:
min(空集) = +∞→ 返回MAXmax(空集) = -∞→ 返回MIN
这样可以避免 panic,提供更好的默认行为。
Q4: 如何处理 f64 类型?
A: 使用引用参数,宏会自动转换:
Q5: memo_block! 每次都重新计算吗?
A:
- 单次调用内:缓存正常工作,避免重复计算 ✓
- 调用结束后:自动清空,释放内存 ✓
- 下次调用:重新计算,但仍然使用缓存优化 ✓
例如:
fib; // 计算 11 次(vs 不使用 177 次)✓
fib; // 再次计算 11 次(vs 不使用 177 次)✓
性能对比示例
Fibonacci 性能测试
use memo;
use Instant;
最佳实践总结
- 默认使用
ref模式:最佳的性能/功能平衡 - 单次计算用
memo_block!:自动清理,避免内存泄漏 - 长期缓存用
#[memo]:跨调用保留,提升性能 - 递归传递相同引用用
ptr:最快 - 复杂类型用
val:功能最完整 - 避免副作用:只在纯函数上使用
- 监控内存:参数范围大时使用
memo_block!
更新日志
v0.1.7
- ✅
ref模式:先比地址,再比内容(最佳平衡) - ✅ 参数重命名:
thread_mode→thread,index_mode→key - ✅ 键模式重命名:
light→ptr,normal→ref,heavy→val - ✅ 线程模式重命名:
local→single - ✅
ptr模式改进:使用 (地址, 长度) 作为键 - ✅
min!/max!空迭代器返回边界值 - ✅
memo_block!支持每个函数独立配置 - ✅ 支持命名参数语法:
key=value - ✅ RefKey 自定义类型:统一处理所有
&T
许可证
MIT 或 Apache-2.0 双许可证。