Rust缓存优化库memoize的使用:函数记忆化技术提升重复计算性能
memoize
一个用于简单Rust函数的 #[memoize]
属性:即具有一个或多个可克隆参数和可克隆返回类型的函数。就是这样。
新闻:该crate已更新,因此您不需要单独导入 lru
、lazy_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
}
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); // 快速返回缓存结果
});
}
注意事项
- 参数必须实现Hash和Eq trait:所有参数类型都需要实现这两个trait
- 返回值必须实现Clone:因为需要缓存和返回克隆的值
- 线程安全:默认缓存是线程安全的,可以在多线程环境中使用
- 内存使用:注意缓存大小,避免内存泄漏
性能建议
- 对于计算成本高的函数使用记忆化
- 合理设置缓存容量,避免占用过多内存
- 对于参数组合较多的情况,考虑使用其他缓存策略
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前后的性能差异。