Rust缓存优化库memoize的使用:函数记忆化技术提升重复计算性能

memoize

一个用于简单Rust函数的 #[memoize] 属性:即具有一个或多个可克隆参数和可克隆返回类型的函数。就是这样。

新闻:该crate已更新,因此您不需要单独导入 lrulazy_static 和其他依赖项。现在一切都应该自动工作。请记住启用 full 功能以使用LRU缓存和其他附加功能。

阅读文档(cargo doc --open)了解详细信息,或查看 examples/ 目录,如果您想了解更多:

// 来自 examples/test2.rs

use memoize::memoize;

#[memoize]
fn hello(arg: String, arg2: usize) -> bool {
  arg.len()%2 == arg2
}

fn main() {
  // `hello` 在这里只被调用一次。
  assert!(! hello("World".to_string(), 0));
  assert!(! hello("World".to_string(), 0));
  // 有时可能需要原始函数。
  assert!(! memoized_original_hello("World".to_string(), 0));
}

这被扩展为(经过一些简化):

std::thread_local! {
  static MEMOIZED_MAPPING_HELLO : RefCell<HashMap<(String, usize), bool>> = RefCell::new(HashMap::new());
}

pub fn memoized_original_hello(arg: String, arg2: usize) -> bool {
  arg.len() % 2 == arg2
}

#[allow(unused_variables)]
fn hello(arg: String, arg2: usize) -> bool {
  let ATTR_MEMOIZE_RETURN__ = MEMOIZED_MAPPING_HELLO.with(|ATTR_MEMOIZE_HM__| {
    let mut ATTR_MEMOIZE_HM__ = ATTR_MEMOIZE_HM__.borrow_mut();
    ATTR_MEMOIZE_HM__.get(&(arg.clone(), arg2.clone())).cloned()
  });
  if let Some(ATTR_MEMOIZE_RETURN__) = ATTR_MEMOIZE_RETURN__ {
    return ATTR_MEMOIZE_RETURN__;
  }

  let ATTR_MEMOIZE_RETURN__ = memoized_original_hello(arg.clone(), arg2.clone());

  MEMOIZED_MAPPING_HELLO.with(|ATTR_MEMOIZE_HM__| {
    let mut ATTR_MEMOIZE_HM__ = ATTR_MEMOIZE_HM__.borrow_mut();
    ATTR_MEMOIZE_HM__.insert((arg, arg2), ATTR_MEMOIZE_RETURN__.clone());
  });

  r
}

进一步功能

如上例所示,默认情况下每个线程都有自己的缓存。如果您希望每个线程共享同一个缓存,可以指定 SharedCache 选项,如下所示,将缓存包装在 std::sync::Mutex 中。例如:

#[memoize(SharedCache)]
fn hello(key: String) -> ComplexStruct {
  // ...
}

您可以选择使用LRU缓存。实际上,如果您知道一个记忆化函数有无限数量的不同输入,您应该这样做!在这种情况下,像这样使用属性:

// 来自 examples/test1.rs
// 使用 --features=full 编译
use memoize::memoize;

#[derive(Debug, Clone)]
struct ComplexStruct {
  // ...
}

#[memoize(Capacity: 123)]
fn hello(key: String) -> ComplexStruct {
  // ...
}

添加更多缓存和配置选项相对简单,主要是解析属性参数的问题。目前,如果您使用诸如 Capacity 之类的参数而没有启用 full 功能,编译将失败。

另一个参数是TimeToLive,指定缓存值允许存活的时间:

#[memoize(Capacity: 123, TimeToLive: Duration::from_secs(2))]

chrono::Duration 也是可能的,但必须首先转换为 std::time::Duration

#[memoize(TimeToLive: chrono::Duration::hours(3).to_std().unwrap())]

缓存值永远不会比提供的持续时间更旧,并在下一个请求时重新计算。

您还可以指定自定义哈希器,如使用 CustomHasher 的AHash。

#[memoize(CustomHasher: ahash::HashMap)]

由于一些哈希器的初始化函数不是 new(),您可以指定 HasherInit 函数调用:

#[memoize(CustomHasher: FxHashMap, HasherInit: FxHashMap::default())]

有时,您不能或不想将数据存储为缓存的一部分。在这些情况下,您可以在 #[memoize] 宏中使用 Ignore 参数来忽略一个参数。任何被 Ignore 的参数不再需要可克隆,因为它们不作为参数集的一部分存储,并且更改被 Ignore 的参数不会再次触发调用该函数。您可以通过多次指定 Ignore 参数来忽略多个参数。

// `Ignore: count_calls` 让我们的函数接受一个 `&mut u32` 参数,这通常是不可能的,因为它不可克隆。
#[memoize(Ignore: count_calls)]
fn add(a: u32, b: u32, count_calls: &mut u32) -> u32 {
    // 跟踪底层函数被调用的次数。
	*count_calls += 1;
	a + b
}

刷新

如果您记忆化一个函数 f,将会有一个名为 memoized_flush_f() 的函数,允许您清除记忆化缓存。

贡献

…总是受欢迎的!这是我第一个过程宏crate,我对功能和风格的改进表示感谢。请发送拉取请求,如果我需要一段时间来审查,请不要气馁;我有时在这里有点慢 :) – Lewin

完整示例代码:

// 完整示例:使用memoize库进行函数记忆化
use memoize::memoize;

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

fn main() {
    // 第一次调用会进行计算
    println!("fibonacci(10) = {}", fibonacci(10));
    
    // 第二次调用相同的参数会直接从缓存中返回结果
    println!("fibonacci(10) = {}", fibonacci(10));
    
    // 不同的参数会触发新的计算
    println!("fibonacci(15) = {}", fibonacci(15));
    
    // 可以刷新特定函数的缓存
    memoized_flush_fibonacci();
    
    // 刷新后再次调用会重新计算
    println!("fibonacci(10) = {}", fibonacci(10));
}

// 使用LRU缓存的示例(需要启用full特性)
#[memoize(Capacity: 100)]
fn expensive_computation(input: String) -> usize {
    // 模拟昂贵的计算
    std::thread::sleep(std::time::Duration::from_millis(100));
    input.len() * 2
}

// 使用共享缓存的示例
#[memoize(SharedCache)]
fn shared_computation(x: i32) -> i32 {
    x * x
}

// 忽略某些参数的示例
#[memoize(Ignore: counter)]
fn computation_with_counter(a: i32, b: i32, counter: &mut u32) -> i32 {
    *counter += 1;
    a + b
}

1 回复

Rust缓存优化库memoize的使用指南

概述

memoize是一个Rust函数记忆化库,通过缓存函数调用的结果来避免重复计算,特别适用于计算密集型或递归函数。该库使用LRU缓存策略,能够显著提升重复计算的性能。

安装方法

在Cargo.toml中添加依赖:

[dependencies]
memoize = "0.3"

基本使用方法

1. 简单函数记忆化

use memoize::memoize;

#[memoize]
fn fibonacci(n: u64) -> u64 {
    if n <= 1 {
        return n;
    }
    fibonacci(n - 1) + fibonacci(n - 2)
}

fn main() {
    // 第一次计算会执行实际计算
    println!("fib(10) = {}", fibonacci(10)); // 输出: fib(10) = 55
    
    // 第二次调用相同参数会直接从缓存返回结果
    println!("fib(10) = {}", fibonacci(10)); // 输出: fib(10) = 55
}

2. 带参数的函数记忆化

use memoize::memoize;

#[memoize]
fn expensive_calculation(x: i32, y: i32) -> i32 {
    println!("执行计算: {} + {}", x, y);
    x + y
}

fn main() {
    println!("结果: {}", expensive_calculation(5, 3)); // 输出: 执行计算: 5 + 3 和 结果: 8
    println!("结果: {}", expensive_calculation(5, 3)); // 直接输出: 结果: 8 (无计算输出)
}

3. 自定义缓存容量

use memoize::memoize;

#[memoize(Capacity: 10)] // 设置LRU缓存容量为10个条目
fn cached_power(base: i32, exponent: i32) -> i32 {
    println!("计算 {}^{}", base, exponent);
    base.pow(exponent as u32)
}

fn main() {
    for i in 1..=15 {
        cached_power(2, i);
    }
    // 只会保留最近10次调用的结果
}

4. 使用Hash类型作为参数

use memoize::memoize;
use std::collections::HashMap;

#[memoize]
fn process_map(map: HashMap<String, i32>) -> i32 {
    map.values().sum()
}

fn main() {
    let mut map = HashMap::new();
    map.insert("a".to_string(), 1);
    map.insert("b".to_string(), 2);
    
    println!("总和: {}", process_map(map.clone()));
    println!("总和: {}", process_map(map)); // 从缓存获取
}

高级特性

5. 带生命周期的函数

use memoize::memoize;

#[memoize]
fn string_length(s: &str) -> usize {
    println!("计算字符串长度: {}", s);
    s.len()
}

fn main() {
    let text = "hello";
    println!("长度: {}", string_length(text));
    println!("长度: {}", string_length(text)); // 缓存命中
}

6. 异步函数记忆化

use memoize::memoize;
use tokio::runtime::Runtime;

#[memoize]
async fn async_calculation(x: i32) -> i32 {
    tokio::time::sleep(std::time::Duration::from_millis(100)).await;
    x * 2
}

fn main() {
    let rt = Runtime::new().unwrap();
    rt.block_on(async {
        println!("结果: {}", async_calculation(5).await);
        println!("结果: {}", async_calculation(5).await); // 快速返回缓存结果
    });
}

注意事项

  1. 参数必须实现Hash和Eq trait:所有参数类型都需要实现这两个trait
  2. 返回值必须实现Clone:因为需要缓存和返回克隆的值
  3. 线程安全:默认缓存是线程安全的,可以在多线程环境中使用
  4. 内存使用:注意缓存大小,避免内存泄漏

性能建议

  • 对于计算成本高的函数使用记忆化
  • 合理设置缓存容量,避免占用过多内存
  • 对于参数组合较多的情况,考虑使用其他缓存策略

memoize库通过简单的属性宏即可实现函数记忆化,是提升Rust应用性能的有效工具。

完整示例demo

// 完整示例:使用memoize库优化计算密集型函数
use memoize::memoize;
use std::collections::HashMap;
use tokio::runtime::Runtime;

// 示例1:斐波那契数列计算
#[memoize]
fn fibonacci(n: u64) -> u64 {
    if n <= 1 {
        return n;
    }
    fibonacci(n - 1) + fibonacci(n - 2)
}

// 示例2:带多个参数的计算函数
#[memoize]
fn expensive_calculation(x: i32, y: i32, z: i32) -> i32 {
    println!("执行复杂计算: {} * {} + {}", x, y, z);
    x * y + z
}

// 示例3:自定义缓存容量
#[memoize(Capacity: 5)]
fn cached_calculation(n: i32) -> i32 {
    println!("计算: {}", n);
    n * n
}

// 示例4:使用HashMap作为参数
#[memoize]
fn process_data_map(map: HashMap<String, i32>) -> i32 {
    println!("处理HashMap数据");
    map.values().sum()
}

// 示例5:字符串处理函数
#[memoize]
fn process_string(s: String) -> usize {
    println!("处理字符串: {}", s);
    s.len()
}

// 示例6:异步计算函数
#[memoize]
async fn async_computation(value: i32) -> i32 {
    tokio::time::sleep(std::time::Duration::from_millis(50)).await;
    value * 3
}

fn main() {
    println!("=== 斐波那契数列示例 ===");
    println!("fib(10) = {}", fibonacci(10));
    println!("fib(10) = {}", fibonacci(10)); // 从缓存获取
    
    println!("\n=== 多参数计算示例 ===");
    println!("结果: {}", expensive_calculation(2, 3, 4));
    println!("结果: {}", expensive_calculation(2, 3, 4)); // 缓存命中
    
    println!("\n=== 自定义缓存容量示例 ===");
    for i in 1..=10 {
        cached_calculation(i);
    }
    // 只保留最近5个结果
    
    println!("\n=== HashMap参数示例 ===");
    let mut data = HashMap::new();
    data.insert("key1".to_string(), 10);
    data.insert("key2".to_string(), 20);
    println!("总和: {}", process_data_map(data.clone()));
    println!("总和: {}", process_data_map(data)); // 缓存命中
    
    println!("\n=== 字符串处理示例 ===");
    let text = "Hello, Memoize!".to_string();
    println!("长度: {}", process_string(text.clone()));
    println!("长度: {}", process_string(text)); // 缓存命中
    
    println!("\n=== 异步计算示例 ===");
    let rt = Runtime::new().unwrap();
    rt.block_on(async {
        println!("异步结果: {}", async_computation(7).await);
        println!("异步结果: {}", async_computation(7).await); // 快速返回
    });
    
    println!("\n=== 性能对比示例 ===");
    use std::time::Instant;
    
    // 无缓存的计算时间
    let start = Instant::now();
    for _ in 0..100 {
        expensive_calculation(5, 6, 7);
    }
    println!("无缓存计算时间: {:?}", start.elapsed());
    
    // 有缓存的计算时间(第二次调用)
    let start = Instant::now();
    for _ in 0..100 {
        expensive_calculation(5, 6, 7);
    }
    println!("有缓存计算时间: {:?}", start.elapsed());
}

这个完整示例展示了memoize库的各种用法,包括基本函数记忆化、多参数处理、自定义缓存容量、复杂类型参数、字符串处理以及异步函数。最后还包含了性能对比测试,展示了使用memoize前后的性能差异。

回到顶部