mau

Crates.iomau
lib.rsmau
version0.1.16
created_at2025-08-22 18:57:54.962665+00
updated_at2025-10-26 12:58:21.226648+00
descriptionA Rust procedural macro library for memoization with MauQueue optimization
homepagehttps://github.com/Flyshde/mau
repositoryhttps://github.com/Flyshde/mau
max_upload_size
id1806740
size290,299
zhangyang (Flyshde)

documentation

https://docs.rs/mau

README

Mau

一个强大的 Rust 过程宏库,提供记忆化(memoization)功能和高效的范围操作宏。

功能特性

  • 自动记忆化: #[memo] 属性宏,智能缓存管理
  • 智能清理: solve! 宏,自动清空缓存,避免内存泄漏
  • 生命周期控制: lifetime 参数,精确控制缓存保留策略
  • 智能缓存键: 三种键模式(ptrrefval),平衡性能和功能
  • 线程模式: 单线程(single)和多线程(multi)支持
  • 范围宏: min!max!sum!and!or!reduce!fold! 等高效宏
  • 灵活语法: 支持多参数、数组、范围等多种调用方式
  • 空迭代器处理: min!max! 对空迭代器返回边界值

安装

[dependencies]
mau = "0.1.16"

快速开始

1. 基础记忆化

use mau::memo;

#[memo]
fn fibonacci(n: u64) -> u64 {
    match n {
        0 | 1 => n,
        _ => fibonacci(n - 1) + fibonacci(n - 2),
    }
}

fn main() {
    println!("fibonacci(40) = {}", fibonacci(40)); // 极快!
}

性能提升

  • 不使用 memo:~1 秒
  • 使用 memo:~0.01 毫秒
  • 性能提升:100,000 倍!

2. 智能清理缓存

use mau::{memo, solve};

#[memo]
fn compute(n: i32) -> i32 {
    // 复杂计算
    n * n * n
}

fn main() {
    // 使用 solve! 宏,自动清空缓存
    let result = solve!(compute(100));
    println!("结果: {}", result);
    // 调用结束,缓存已清空,避免内存泄漏
}

3. 范围宏

use mau::{min, max, sum, fold, each};

fn main() {
    let data = vec![3, 1, 4, 1, 5, 9, 2, 6];
    
    // 多种语法支持
    println!("最小值: {}", min!(data));        // 数组语法:1
    println!("最小值: {}", min!(1, 2, 3));     // 多参数语法:1
    println!("最大值: {}", max!(data));        // 9
    println!("总和: {}", sum!(data));          // 31
    
    // 范围语法
    println!("部分最小: {}", min!(|i| data[i], 2..5));  // 1
    
    // fold 累积操作
    let product = fold!(1, |i| data[i], 0..data.len(), |acc, val| acc * val);
    println!("乘积: {}", product);  // 51840
    
    // each! 遍历执行
    let mut results = Vec::new();
    each!(|i| {
        results.push(data[i] * 2);
    }, 0..data.len());
    println!("双倍结果: {:?}", results);
    
    // 空迭代器返回边界值
    let empty: Vec<i32> = vec![];
    println!("空数组的最小值: {}", min!(empty));  // i32::MAX
}

核心功能详解

#[memo] - 自动记忆化

为函数添加记忆化,自动缓存计算结果:

#[memo]
fn fibonacci(n: usize) -> usize {
    if n <= 1 { n } else { fibonacci(n - 1) + fibonacci(n - 2) }
}

fn main() {
    // 第一次调用:计算并缓存
    let r1 = fibonacci(10);  // 计算
    
    // 第二次调用:直接从缓存返回
    let r2 = fibonacci(10);  // 命中缓存,极快
    
    // 利用已有缓存计算新值
    let r3 = fibonacci(11);  // 利用 fibonacci(10) 和 fibonacci(9) 的缓存
}

生成的辅助函数

#[memo]
fn fibonacci(n: usize) -> usize {
    // ...
}

// 自动生成:
// - fibonacci_start(n) : 调用并清空缓存
// - fibonacci_clear()  : 手动清空缓存

solve! 宏 - 智能清理

自动清空缓存,避免内存泄漏:

use mau::{memo, solve};

#[memo]
fn is_even(n: usize) -> bool {
    match n {
        0 => true,
        _ => is_odd(n - 1),
    }
}

#[memo]
fn is_odd(n: usize) -> bool {
    match n {
        0 => false,
        _ => is_even(n - 1),
    }
}

fn main() {
    // 简洁语法:调用并自动清理缓存
    let result = solve!(is_even(100));
    
    // 支持复杂表达式
    let (r1, r2) = solve!((is_even(50), is_odd(51)));
    
    // 支持代码块
    let result = solve!({
        let a = is_even(10);
        let b = is_even(20);
        a && b
    });
    
    // 嵌套调用(自动递归替换)
    let result = solve!(is_even(is_odd(5) as usize * 10));
}

何时使用 solve!

应该使用

  • 单次调用中有大量递归(如动态规划)
  • 参数范围很大,不需要跨调用缓存
  • 需要控制内存使用

不应该使用

  • 需要长期保留缓存(跨多次调用)
  • 参数经常重复,缓存命中率高

手动清理缓存

#[memo]
fn compute(n: i32) -> i32 {
    n * n * n
}

fn main() {
    // 方式 1: 使用 start! 自动清理
    solve!(compute(100));
    
    // 方式 2: 手动清理
    compute(100);
    compute_clear();  // 手动清空缓存
}

参数配置

线程模式(thread

  • single(默认):单线程,性能最佳
  • multi:多线程安全,全局共享

键模式(key

  • ptr(默认):地址+长度,最快
  • ref:先比地址+长度,再比内容,平衡性能
  • val:深度比较,功能最完整

生命周期模式(lifetime

  • problem(默认):每次 _start() 调用后清除缓存
  • program:保留缓存直到程序结束(仅在键不包含地址时有效)

使用语法

// 使用默认配置
#[memo]
fn calc(n: i32) -> i32 { n * n }

// 命名参数(推荐)
#[memo(thread=single, key=ref)]
#[memo(thread=multi, key=ptr)]
#[memo(key=val, lifetime=problem)]
#[memo(key=val, lifetime=program)]  // 长期保留缓存(需要 key=val)

键模式详解

ptr 模式 - 最快,地址+长度

#[memo(key=ptr)]
fn process(data: &[i32]) -> i32 {
    data.iter().sum()
}

// 示例:
let arr = vec![1, 2, 3];
process(&arr);  // 第1次:计算
process(&arr);  // 第2次:命中 ✓(相同地址+长度)

let arr2 = vec![1, 2, 3];  // 内容相同,地址不同
process(&arr2);  // 第3次:重新计算(地址不同)

// 切片长度不同
process(&arr[..2]);  // 第4次:重新计算(长度不同)

缓存键(地址, 长度) 何时使用:相同引用会反复调用(如递归中传递同一个数组)

ref 模式,先比地址+长度,再比内容

#[memo(key=ref)]  // 或 #[memo]
fn process(data: &[i32]) -> i32 {
    data.iter().sum()
}

// 示例:
let arr = vec![1, 2, 3];
process(&arr);  // 第1次:计算
process(&arr);  // 第2次:命中 ✓(地址+长度相等)

let arr2 = vec![1, 2, 3];  // 内容相同,地址不同
process(&arr2);  // 第3次:命中 ✓(地址不等,但内容相等)

let arr3 = vec![4, 5, 6];  // 内容不同
process(&arr3);  // 第4次:重新计算(内容不等)

// 切片长度不同
process(&arr[..2]);  // 第5次:重新计算(长度不同)

工作原理

  1. 快速路径:比较 (地址, 长度),相等则命中
  2. 慢速路径:地址或长度不等时,比较内容

何时使用:大部分情况的最佳选择

val 模式 - 功能最完整,深度比较

#[memo(key=val)]
fn process(matrix: &[Vec<i32>]) -> i32 {
    matrix.iter().map(|row| row.iter().sum::<i32>()).sum()
}

何时使用:复杂嵌套类型,需要深度比较

三种模式对比

模式 比较方式 相同地址+长度 不同地址+相同内容 性能 适用场景
ptr 地址+长度 ⚡极快 ❌不命中 最快 一般情况(默认)
ref 先比地址+长度,若相等则命中;否则比内容 ⚡快 ✅命中 内容可能重复
val 深度比较 ✅命中 复杂嵌套类型

生命周期模式详解

lifetime 参数控制缓存何时被清除,这对内存管理至关重要。

problem 模式(默认)- 问题级别缓存

#[memo(lifetime=problem)]  // 或 #[memo]
fn solve_subproblem(data: &[i32]) -> i32 {
    data.iter().sum()
}

fn main() {
    let data = vec![1, 2, 3, 4, 5];
    
    // 使用 solve! 或 _start(),调用后自动清除缓存
    let result = solve!(solve_subproblem(&data));
    // 缓存已清除,不占用内存
}

何时使用

  • ✅ 单次问题求解(如 OJ 题目、一次性计算)
  • ✅ 需要控制内存使用
  • ✅ 参数范围很大,不需要跨问题复用

program 模式 - 程序级别缓存

#[memo(key=val, lifetime=program)]
fn expensive_calculation(n: i32, m: i32) -> i32 {
    // 复杂计算...
    n * n + m * m
}

fn main() {
    // 多次调用,缓存一直保留
    let r1 = expensive_calculation_start(3, 4);  // 计算
    let r2 = expensive_calculation_start(3, 4);  // 命中缓存
    let r3 = expensive_calculation_start(3, 4);  // 命中缓存
    // 缓存保留到程序结束,可以跨问题复用
}

何时使用

  • ✅ 配置解析、数据计算等需要长期复用的结果
  • ✅ 多次请求/调用相同参数值
  • ✅ 缓存命中率高,相同输入会在不同问题中重复出现

重要:program 模式的生效条件

lifetime=program 只有在缓存键不包含地址信息时才会保留缓存

函数参数类型 key 模式 lifetime=program 是否保留缓存 原因
有引用参数 (如 &[i32]) ptr ❌ 清除 键包含地址,旧地址无法复用,浪费内存
有引用参数 (如 &[i32]) ref ❌ 清除 键包含地址,旧地址无法复用
有引用参数 (如 &[i32]) val ✅ 保留 键只基于值,可跨问题复用
无引用参数 (如 i32) ptr ✅ 保留 无引用,键完全基于值
无引用参数 (如 i32) ref ✅ 保留 无引用,键完全基于值
无引用参数 (如 i32) val ✅ 保留 键完全基于值

示例说明

// ✅ 会保留缓存:无引用参数,键完全基于值
#[memo(lifetime=program)]
fn calculate(n: i32) -> i32 {
    n * n  // 相同 n 可跨问题复用缓存
}

// ✅ 会保留缓存:有引用参数 + key=val,键基于值
#[memo(key=val, lifetime=program)]
fn process_val(data: &[i32]) -> i32 {
    data.iter().sum()  // 内容相同的数组可跨问题复用
}

// ❌ 不会保留缓存:有引用参数 + key=ptr,键包含地址
#[memo(key=ptr, lifetime=program)]
fn process_array(data: &[i32]) -> i32 {
    data.iter().sum()  // 每次调用 _start() 后会清除(避免内存浪费)
}

// ❌ 不会保留缓存:有引用参数 + key=ref,键包含地址
#[memo(key=ref, lifetime=program)]
fn process_data(data: &[i32]) -> i32 {
    data.iter().sum()  // 每次调用 _start() 后会清除
}

为什么这样设计?

情况1:键包含地址(ptr/ref 模式 + 引用参数)

  • 每个问题的数组地址不同,旧缓存无法被新问题访问
  • 保留缓存只会浪费内存,因此即使设置 program 也会清除
#[memo(key=ptr, lifetime=program)]
fn solve(arr: &[i32]) -> i32 {
    arr.iter().sum()
}

fn main() {
    // 问题1
    let data1 = vec![1, 2, 3];  // 地址 0x1000
    solve_start(&data1);  // 缓存键: (0x1000, 3) -> 结果 6
    
    // 问题2
    let data2 = vec![1, 2, 3];  // 地址 0x2000(不同地址!)
    solve_start(&data2);  // 缓存键: (0x2000, 3),无法命中旧缓存
    
    // 旧缓存 (0x1000, 3) 永远不会再被访问,白白占用内存
    // 所以自动清除缓存
}

情况2:键不包含地址(val 模式或无引用参数)

  • 键完全基于值,相同输入可以跨问题复用
  • 保留缓存可以提高性能
#[memo(key=val, lifetime=program)]
fn solve(arr: &[i32]) -> i32 {
    arr.iter().sum()
}

fn main() {
    // 问题1
    let data1 = vec![1, 2, 3];
    solve_start(&data1);  // 缓存键: [1,2,3] -> 结果 6
    
    // 问题2:不同数组,但内容相同
    let data2 = vec![1, 2, 3];
    solve_start(&data2);  // 缓存键: [1,2,3],命中缓存!✓
    
    // 缓存被复用,提高性能
}

使用场景

场景 1: 动态规划(使用 start! 自动清理)

use mau::{memo, solve};

#[memo(key=ref)]
fn merge_stones(data: &[usize]) -> usize {
    match data.len() {
        0 | 1 => 0,
        _ => {
            let mut min_cost = usize::MAX;
            for i in 1..data.len() {
                let left = merge_stones(&data[..i]);
                let right = merge_stones(&data[i..]);
                let cost = left + right + data.iter().sum::<usize>();
                min_cost = min_cost.min(cost);
            }
            min_cost
        }
    }
}

fn main() {
    let stones = vec![1, 2, 3, 4, 5];
    
    // 使用 start! 自动清理缓存
    let result = solve!(merge_stones(&stones));
    println!("最小成本: {}", result);
    // 缓存已清空,不会占用内存
}

场景 2: Web 服务(长期缓存)

#[memo]
fn get_user_info(user_id: i32) -> UserInfo {
    // 数据库查询
    database.query(user_id)
}

fn handle_request(user_id: i32) {
    // 多次调用,利用缓存避免重复查询
    let info = get_user_info(user_id);
    // 缓存保留,下次请求直接命中
}

场景 3: 互相递归

use mau::{memo, solve};

#[memo]
fn is_even(n: usize) -> bool {
    if n == 0 { true } else { is_odd(n - 1) }
}

#[memo]
fn is_odd(n: usize) -> bool {
    if n == 0 { false } else { is_even(n - 1) }
}

fn main() {
    // 方式 1: 普通调用(缓存持续)
    let r1 = is_even(100);
    let r2 = is_even(100);  // 命中缓存
    
    // 方式 2: 使用 start! 清理
    let r3 = solve!(is_even(100));
    // 缓存已清空
    
    // 方式 3: 手动清理
    is_even_clear();
    is_odd_clear();
}

范围宏

高效的范围聚合操作,支持多种灵活的调用语法。

多种语法支持

use mau::{min, max, sum, and, or};

fn main() {
    let data = vec![3, 1, 4, 1, 5, 9, 2, 6];
    
    // 1. 多参数语法(2个或更多参数)
    println!("{}", min!(1, 2));           // 1
    println!("{}", max!(1, 2, 3));        // 3
    println!("{}", sum!(1, 2, 3, 4));     // 10
    
    // 2. 数组简写语法
    println!("{}", min!(data));           // 1
    println!("{}", max!(data));           // 9
    println!("{}", sum!(data));           // 31
    
    // 3. 范围语法 - 部分范围
    println!("{}", min!(|i| data[i], 2..5));  // 索引 2~4 的最小值
    
    // 4. 范围语法 - 表达式
    println!("{}", min!(|i| data[i] * data[i], 0..data.len()));  // 平方的最小值
    
    // 5. 包含范围(闭区间)
    println!("{}", sum!(|i| data[i], 2..=4));  // 索引 2,3,4 的和
    
    // 6. 迭代器语法
    println!("{}", min!(|x| x, data.iter()));  // 1
    
    // 7. 布尔运算
    let all_positive = and!(|i| data[i] > 0, 0..data.len());
    println!("是否全部为正: {}", all_positive);  // true
}

fold! 宏 - 自定义累积操作

fold! 提供了最灵活的累积操作:

use mau::fold;
use std::collections::HashMap;

fn main() {
    let data = vec![1, 2, 3, 4, 5];
    
    // 基础:求和(初始值为 0)
    let sum = fold!(0, |i| data[i], 0..data.len(), |acc, val| acc + val);
    println!("和: {}", sum);  // 15
    
    // 求积(初始值为 1)
    let product = fold!(1, |i| data[i], 0..data.len(), |acc, val| acc * val);
    println!("积: {}", product);  // 120
    
    // 构建字符串
    let words = vec!["Hello", "World", "Rust"];
    let sentence = fold!(String::new(), |i| words[i], 0..words.len(), 
        |mut acc: String, val: &str| {
            if !acc.is_empty() { acc.push(' '); }
            acc.push_str(val);
            acc
        }
    );
    println!("{}", sentence);  // "Hello World Rust"
    
    // 构建 HashMap
    let keys = vec!["a", "b", "c"];
    let values = vec![1, 2, 3];
    let map = fold!(HashMap::new(), |i| (keys[i], values[i]), 0..keys.len(),
        |mut acc: HashMap<&str, i32>, (k, v)| {
            acc.insert(k, v);
            acc
        }
    );
    
    // 同时计算多个统计量(使用元组)
    let (sum, count, max) = fold!(
        (0, 0, i32::MIN), 
        |i| data[i], 
        0..data.len(),
        |(s, c, m), val| (s + val, c + 1, m.max(val))
    );
    let avg = sum / count;
    println!("平均: {}, 最大: {}", avg, max);
    
    // 条件过滤累积(只累加偶数)
    let even_sum = fold!(0, |i| data[i], 0..data.len(), |acc, val| {
        if val % 2 == 0 { acc + val } else { acc }
    });
    println!("偶数和: {}", even_sum);  // 6 (2 + 4)
}

fold! vs reduce!

特性 fold! reduce!
初始值 需要提供 使用第一个元素
空序列 返回初始值 panic
累加器类型 可与元素类型不同 必须相同
灵活性
use mau::{fold, reduce};

let data = vec![1, 2, 3, 4, 5];

// reduce: 使用第一个元素作为初始值
let sum1 = reduce!(|i| data[i], 0..data.len(), |a, b| a + b);
// 相当于: 1 + 2 + 3 + 4 + 5 = 15

// fold: 提供初始值
let sum2 = fold!(0, |i| data[i], 0..data.len(), |acc, val| acc + val);
// 相当于: 0 + 1 + 2 + 3 + 4 + 5 = 15

// fold 的优势:可以处理空序列
let empty: Vec<i32> = vec![];
let result = fold!(100, |i| empty[i], 0..0, |acc, val| acc + val);
println!("{}", result);  // 100(返回初始值)

// reduce 会 panic
// let result = reduce!(|i| empty[i], 0..0, |a, b| a + b);  // panic!

空迭代器处理

let empty: Vec<i32> = vec![];

// min! 返回类型的 MAX 值
println!("{}", min!(empty));  // i32::MAX = 2147483647

// max! 返回类型的 MIN 值
println!("{}", max!(empty));  // i32::MIN = -2147483648

// sum! 返回 0(加法的单位元)
let empty_sum: Vec<i32> = vec![];
println!("{}", sum!(empty_sum));  // 0

// 浮点数也返回 0.0
let empty_f64: Vec<f64> = vec![];
println!("{}", sum!(empty_f64));  // 0.0

// 不支持的类型会 panic(仅针对 min/max)
let empty_str: Vec<&str> = vec![];
// min!(empty_str);  // panic: "type does not have a MAX value"

支持的类型

  • ✅ 整数:i8i128u8u128isizeusize
  • ✅ 浮点:f32f64
  • ✅ 字符:char(仅 min/max)
  • ❌ 字符串等:运行时 panic(仅 min/max,sum 不支持字符串)

详细示例

动态规划:背包问题

use mau::{memo, solve};

#[memo(key=ref)]
fn knapsack(weights: &[i32], values: &[i32], capacity: i32, n: usize) -> i32 {
    if n == 0 || capacity == 0 {
        return 0;
    }
    
    if weights[n - 1] > capacity {
        knapsack(weights, values, capacity, n - 1)
    } else {
        let include = values[n - 1] + 
            knapsack(weights, values, capacity - weights[n - 1], n - 1);
        let exclude = knapsack(weights, values, capacity, n - 1);
        include.max(exclude)
    }
}

fn main() {
    let weights = vec![10, 20, 30];
    let values = vec![60, 100, 120];
    let capacity = 50;
    
    // 使用 start! 自动清理缓存
    let result = solve!(knapsack(&weights, &values, capacity, weights.len()));
    println!("最大价值: {}", result);  // 220
}

多参数记忆化

use mau::memo;

#[memo(key=ref)]
fn edit_distance(s1: &String, s2: &String, m: usize, n: usize) -> usize {
    if m == 0 { return n; }
    if n == 0 { return m; }
    
    if s1.chars().nth(m - 1) == s2.chars().nth(n - 1) {
        edit_distance(s1, s2, m - 1, n - 1)
    } else {
        1 + [
            edit_distance(s1, s2, m - 1, n),
            edit_distance(s1, s2, m, n - 1),
            edit_distance(s1, s2, m - 1, n - 1),
        ].iter().min().unwrap()
    }
}

fn main() {
    let s1 = "kitten".to_string();
    let s2 = "sitting".to_string();
    let dist = edit_distance(&s1, &s2, s1.len(), s2.len());
    println!("编辑距离: {}", dist);  // 3
}

范围宏高级用法

自定义归约

use mau::reduce;

fn main() {
    let data = vec![1, 2, 3, 4, 5];
    
    // 找最大值
    let max = reduce!(|i| data[i], 0..data.len(), |a, b| a.max(b));
    
    // 字符串连接
    let words = vec!["Hello", " ", "World"];
    let text = reduce!(
        |i| words[i].to_string(),
        0..words.len(),
        |a, b| a + &b
    );
    println!("{}", text);  // "Hello World"
}

短路优化

use mau::{and, or};

fn expensive_check(x: i32) -> bool {
    println!("检查 {}", x);
    x > 0
}

fn main() {
    let data = vec![1, 2, -3, 4, 5];
    
    // and! 遇到第一个 false 就停止
    let all_positive = and!(|i| expensive_check(data[i]), 0..data.len());
    // 输出:
    // 检查 1
    // 检查 2  
    // 检查 -3  <- 停止,不再检查后续元素
    
    println!("全部为正: {}", all_positive);  // false
}

性能数据

记忆化性能提升

算法 规模 不使用 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:递归中传递同一个引用
#[memo(key=ptr)]
fn recursive(data: &[i32], index: usize) -> i32 {
    if index >= data.len() { return 0; }
    data[index] + recursive(data, index + 1)  // 同一个 data
}

// 场景2:不同调用但参数可能相同(内容可能重复)
#[memo(key=ref)]
fn process(data: &[i32]) -> i32 {
    data.iter().sum()
}

// 场景3:复杂嵌套类型
#[memo(key=val)]
fn matrix_calc(matrix: &[Vec<Vec<i32>>]) -> i32 {
    // 深度比较
    0
}

注意事项

1. 避免副作用

// ❌ 错误:有副作用
#[memo]
fn bad_example(n: i32) -> i32 {
    println!("计算 {}", n);  // 缓存命中时不会打印!
    n * 2
}

// ✅ 正确:纯函数
#[memo]
fn good_example(n: i32) -> i32 {
    n * 2
}

2. 参数设计

// ❌ 错误:无关参数导致缓存失效
#[memo]
fn bad_design(n: i32, timestamp: u64) -> i32 {
    n * 2  // timestamp 不影响结果,但会导致缓存失效
}

// ✅ 正确:只包含必要参数
#[memo]
fn good_design(n: i32) -> i32 {
    n * 2
}

3. 内存监控

#[memo]
fn compute(n: i32) -> i32 {
    n * n * n
}

fn main() {
    // 方式 1: 每次清理
    for i in 0..10000 {
        solve!(compute(i));  // 自动清理
    }
    
    // 方式 2: 批量清理
    for i in 0..10000 {
        compute(i);
    }
    compute_clear();  // 手动清理
}

4. f64 类型处理

// ❌ f64 不实现 Hash 和 Eq
// #[memo]
// fn calc(x: f64) -> f64 { x * x }  // 编译错误

// ✅ 使用引用(自动转换为 u64)
#[memo(key=ref)]
fn calc(x: &f64) -> f64 { x * x }

// ✅ 或使用 val 模式
#[memo(key=val)]
fn calc_array(data: &[f64]) -> f64 {
    data.iter().sum()
}

参数速查表

#[memo] 参数

#[memo]                                    // 默认:thread=single, key=ptr, lifetime=problem
#[memo(thread=single, key=ref)]            // 命名参数
#[memo(thread=multi, key=ptr)]             // 多线程 + 地址键
#[memo(key=val)]                           // 只指定 key
#[memo(key=val, lifetime=program)]         // 长期保留缓存(需要 key=val)
#[memo(thread=multi, key=val, lifetime=program)]  // 完整指定

solve! 宏语法

solve!(func(args))                   // 单个函数调用
solve!((func1(a), func2(b)))        // 多个调用(元组)
solve!({ let a = f(); a + 1 })      // 代码块
solve!(f(g(h(x))))                   // 嵌套调用(自动递归替换)

范围宏语法

// min, max, sum, and, or
min!(1, 2)                           // 两参数(新增支持)
min!(1, 2, 3)                        // 多参数
min!(array)                          // 整个数组
min!(|i| array[i], 0..10)           // 范围表达式
min!(|i| array[i] * 2, 0..=9)       // 包含范围(闭区间)
min!(|x| x, array.iter())           // 迭代器

// reduce - 自定义归约
reduce!(|i| data[i], 0..n, |a, b| a.max(b))

// fold - 带初始值的累积
fold!(init_val, |i| data[i], 0..n, |acc, val| acc + val)

常见问题

Q1: ref 模式比 ptr 慢多少?

A: 在缓存已预热的情况下,ref 模式约为 ptr 模式的 80% 性能。但 ref 模式功能更强(内容相同就命中),是大多数情况的最佳选择。

Q2: 为什么 ref 模式需要比较地址和长度?

A: 避免相同地址不同长度的错误命中:

let data = vec![1, 2, 3, 4, 5];
&data[..2]  // addr = data.as_ptr(), len = 2
&data[..5]  // addr = data.as_ptr(), len = 5  ← 地址相同!

// 如果只比地址,会错误地认为这两个切片相同
// 同时比较地址和长度后:(addr1, len1) != (addr2, len2)

Q3: 空迭代器为什么返回边界值?

A: 符合数学定义:

  • min(空集) = +∞ → 返回 MAX
  • max(空集) = -∞ → 返回 MIN

这样可以避免 panic,提供更好的默认行为。

Q4: 如何处理 f64 类型?

A: 使用引用参数,宏会自动转换:

#[memo(key=ref)]
fn calc(x: &f64) -> f64 {
    x * x
}

#[memo(key=ref)]
fn sum_floats(data: &[f64]) -> f64 {
    data.iter().sum()
}

Q5: 为什么我设置了 lifetime=program 但缓存还是被清除?

A: lifetime=program 只在键中不包含地址信息时才生效。检查:

  1. 函数是否有引用参数且使用了 key=ptrkey=ref

    // ❌ 有引用参数 + ptr/ref,键包含地址,program 无效
    #[memo(key=ptr, lifetime=program)]
    fn process(data: &[i32]) -> i32 { data.iter().sum() }
    
    // ✅ 无引用参数,键基于值,program 有效
    #[memo(key=ptr, lifetime=program)]
    fn calc(n: i32) -> i32 { n * n }
    
  2. 是否使用了 key=val

    // ✅ val 模式键只基于值,program 有效
    #[memo(key=val, lifetime=program)]
    fn process(data: &[i32]) -> i32 { data.iter().sum() }
    
    // ❌ ptr/ref 模式键包含地址,program 无效
    #[memo(key=ptr, lifetime=program)]
    fn process(data: &[i32]) -> i32 { data.iter().sum() }
    

总结:只有 (无引用参数) OR (有引用参数 AND key=val) 时,lifetime=program 才会保留缓存。

Q6: min!(1, 2) 两参数语法何时可用?

A: v0.1.12 及以上版本支持。如果遇到错误,请升级:

[dependencies]
mau = "0.1.16"  # 或更高版本

完整示例

use mau::{memo, start, min, max, sum};

// 长期缓存:配置解析
#[memo]
fn parse_config(path: String) -> Config {
    // 读取配置文件(缓存结果)
}

// 临时缓存:动态规划
#[memo(key=ref)]
fn longest_increasing_subsequence(arr: &[i32], i: usize) -> usize {
    if i == 0 { return 1; }
    
    let mut max_len = 1;
    for j in 0..i {
        if arr[j] < arr[i] {
            max_len = max_len.max(1 + longest_increasing_subsequence(arr, j));
        }
    }
    max_len
}

fn main() {
    // 长期缓存
    let config = parse_config("config.toml".to_string());
    
    // 使用 start! 清理临时缓存
    let data = vec![10, 9, 2, 5, 3, 7, 101, 18];
    let result = solve!({
        (0..data.len())
            .map(|i| longest_increasing_subsequence(&data, i))
            .max()
            .unwrap()
    });
    println!("最长递增子序列长度: {}", result);
    
    // 范围宏
    let min_val = min!(data);
    let max_val = max!(data);
    let sum_val = sum!(data);
    println!("最小: {}, 最大: {}, 总和: {}", min_val, max_val, sum_val);
}

最佳实践总结

记忆化配置

  1. 默认使用 key=ptr:性能最佳,适合大多数场景
  2. 内容可能重复用 key=ref:兼顾性能和功能
  3. 复杂嵌套类型用 key=val:功能最完整

生命周期选择

  1. 单次计算用 lifetime=problem(默认):自动清理,避免内存泄漏
  2. 长期缓存用 lifetime=program:跨调用保留,需要满足条件:
    • ✅ 无引用参数(键基于值)
    • ✅ 或者:有引用参数 + 使用 key=val(键基于值)
    • ❌ 不能用 key=ptrkey=ref(键包含地址,会自动清除)

使用技巧

  1. 单次问题求解用 solve!:自动清理,推荐用于 OJ、算法竞赛
  2. Web 服务、配置解析等用 lifetime=program:长期复用
  3. 递归传递相同引用用 ptr:最快
  4. 避免副作用:只在纯函数上使用
  5. 监控内存:参数范围大时使用 solve! 或手动 _clear()

范围宏选择

  1. 简单聚合用 min!/max!/sum!:最简洁
  2. 自定义操作用 reduce!:灵活
  3. 需要初始值用 fold!:最强大,可处理空序列和类型转换
  4. 遍历执行用 each!:对每个元素执行操作

each!

each! 宏用于遍历范围内的每个索引并执行闭包,等价于 for 循环:

use mau::each;

let data = vec![3, 1, 4, 1, 5, 9];
let mut results = Vec::new();

// 等价于 for i in 0..data.len() { results.push(data[i]); }
each!(|i| {
    results.push(data[i]);
}, 0..data.len());

// 输出: [3, 1, 4, 1, 5, 9]
println!("{:?}", results);

支持的语法

  • 范围表达式: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 模式智能缓存清除:使用 ptrref 模式时,通过 solve! 调用会自动清除缓存
    • 避免内存泄漏:地址模式下的缓存无法跨问题复用
    • 提升性能:减少无效缓存占用内存
  • 📝 更新文档:添加 each! 宏和缓存清除策略说明

v0.1.14

  • 🐛 修复 lifetime=program 逻辑:修正了缓存保留/清除的判断条件
    • 正确行为:键不包含地址时保留缓存,键包含地址时清除缓存
    • 无引用参数 或 key=val:保留缓存 ✓
    • 有引用参数 + (key=ptrkey=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_modethreadindex_modekey
  • ✅ 键模式重命名:lightptrnormalrefheavyval
  • ✅ 线程模式重命名:localsingle
  • ptr 模式改进:使用 (地址, 长度) 作为键
  • min!/max! 空迭代器返回边界值

许可证

MIT 或 Apache-2.0 双许可证。

Commit count: 3

cargo fmt