我正在阅读第二版Rust书中的the section on closures。在本节的最后,有一个练习来扩展之前给出的Cacher
实现。我试了一下:
use std::clone::Clone;
use std::cmp::Eq;
use std::collections::HashMap;
use std::hash::Hash;
struct Cacher<T, K, V>
where
T: Fn(K) -> V,
K: Eq + Hash + Clone,
V: Clone,
{
calculation: T,
values: HashMap<K, V>,
}
impl<T, K, V> Cacher<T, K, V>
where
T: Fn(K) -> V,
K: Eq + Hash + Clone,
V: Clone,
{
fn new(calculation: T) -> Cacher<T, K, V> {
Cacher {
calculation,
values: HashMap::new(),
}
}
fn value(&mut self, arg: K) -> V {
match self.values.clone().get(&arg) {
Some(v) => v.clone(),
None => {
self.values
.insert(arg.clone(), (self.calculation)(arg.clone()));
self.values.get(&arg).unwrap().clone()
}
}
}
}
创建一个最终有效的版本后,我真的很不高兴。真正让我感到困惑的是,cacher.value(...)
对clone()
进行了5次(!)调用。有没有办法避免这种情况?
答案 0 :(得分:9)
您的怀疑是正确的,代码包含过多的clone()
来电,打败了Cacher
旨在实现的优化。
开始的是对self.values.clone()
的调用 - 它会在每次访问上创建整个缓存的副本。
删除此克隆。
正如您可能发现的那样,只需删除.clone()
即可编译。这是因为借用检查器会考虑在match
的整个持续时间内引用的地图。 HashMap::get
返回的共享引用指向地图内的项目,这意味着当它存在时,禁止创建另一个对HashMap::insert
所需的同一地图的可变引用。对于要编译的代码,您需要拆分匹配,以便在调用insert
之前强制共享引用超出范围:
// avoids unnecessary clone of the whole map
fn value(&mut self, arg: K) -> V {
if let Some(v) = self.values.get(&arg).map(V::clone) {
return v;
} else {
let v = (self.calculation)(arg.clone());
self.values.insert(arg, v.clone());
v
}
}
这好多了,可能已经足够好了#34;对于大多数实际目的。值已经缓存的热路径现在只包含一个克隆,而实际需要一个克隆,因为原始值必须保留在哈希映射中。 (另请注意,克隆并不需要昂贵或暗示深度复制 - 存储的值可以是Rc<RealValue>
,它可以免费购买对象共享。在这种情况下,clone()
只会增加对象的引用计数。)
如果缓存未命中,则必须克隆密钥,因为calculation
被声明为使用它。但是,单个克隆就足够了,因此我们可以将原始arg
传递给insert
,而无需再次克隆它。然而,密钥克隆仍然没有必要 - 计算功能不应该要求它正在转换的密钥的所有权。删除此克隆归结为修改计算函数的签名以通过引用获取密钥。将T
的特征范围更改为T: Fn(&K) -> V
可以得到value()
的以下公式:
// avoids unnecessary clone of the key
fn value(&mut self, arg: K) -> V {
if let Some(v) = self.values.get(&arg).map(V::clone) {
return v;
} else {
let v = (self.calculation)(&arg);
self.values.insert(arg, v.clone());
v
}
}
现在只剩下两次clone()
调用,每个代码路径一次。就值克隆而言,这是最佳的,但细心的读者仍然会被一个细节所困扰:如果缓存未命中,哈希表查找将有效地发生两次同一个键:一次拨打HashMap::get
,然后再次访问HashMap::insert
。如果我们可以重新使用第一次完成的工作并且只执行一次哈希映射查找,那将是很好的。这可以通过将get()
和insert()
替换为entry()
:
// avoids the second lookup on cache miss
fn value(&mut self, arg: K) -> V {
match self.values.entry(arg) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
let v = (self.calculation)(entry.key());
entry.insert(v)
}
}.clone()
}
我们也抓住机会在比赛结束后转移.clone()
来电。
可运行的示例in the playground。
答案 1 :(得分:1)
我正在解决相同的练习并以下面的代码结束:
use std::thread;
use std::time::Duration;
use std::collections::HashMap;
use std::hash::Hash;
use std::fmt::Display;
struct Cacher<P, R, T>
where
T: Fn(&P) -> R,
P: Eq + Hash + Clone,
{
calculation: T,
values: HashMap<P, R>,
}
impl<P, R, T> Cacher<P, R, T>
where
T: Fn(&P) -> R,
P: Eq + Hash + Clone,
{
fn new(calculation: T) -> Cacher<P, R, T> {
Cacher {
calculation,
values: HashMap::new(),
}
}
fn value<'a>(&'a mut self, key: P) -> &'a R {
let calculation = &self.calculation;
let key_copy = key.clone();
self.values
.entry(key_copy)
.or_insert_with(|| (calculation)(&key))
}
}
它只在value()
方法中生成密钥的单个副本。它不会复制结果值,而是返回带有生命周期说明符的引用,该引用等于封闭Cacher
实例的生命周期(我认为这是合乎逻辑的,因为映射中的值将继续存在直到Cacher
本身被删除。)
这是一个测试程序:
fn main() {
let mut cacher1 = Cacher::new(|num: &u32| -> u32 {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
*num
});
calculate_and_print(10, &mut cacher1);
calculate_and_print(20, &mut cacher1);
calculate_and_print(10, &mut cacher1);
let mut cacher2 = Cacher::new(|str: &&str| -> usize {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
str.len()
});
calculate_and_print("abc", &mut cacher2);
calculate_and_print("defghi", &mut cacher2);
calculate_and_print("abc", &mut cacher2);
}
fn calculate_and_print<P, R, T>(intensity: P, cacher: &mut Cacher<P, R, T>)
where
T: Fn(&P) -> R,
P: Eq + Hash + Clone,
R: Display,
{
println!("{}", cacher.value(intensity));
}
及其输出:
calculating slowly...
10
calculating slowly...
20
10
calculating slowly...
3
calculating slowly...
6
3
答案 2 :(得分:1)
如果您删除了返回值的要求,则无需使用Entry
执行任何克隆:
use std::{
collections::{hash_map::Entry, HashMap},
fmt::Display,
hash::Hash,
thread,
time::Duration,
};
struct Cacher<P, R, T>
where
T: Fn(&P) -> R,
P: Eq + Hash,
{
calculation: T,
values: HashMap<P, R>,
}
impl<P, R, T> Cacher<P, R, T>
where
T: Fn(&P) -> R,
P: Eq + Hash,
{
fn new(calculation: T) -> Cacher<P, R, T> {
Cacher {
calculation,
values: HashMap::new(),
}
}
fn value<'a>(&'a mut self, key: P) -> &'a R {
let calculation = &self.calculation;
match self.values.entry(key) {
Entry::Occupied(e) => e.into_mut(),
Entry::Vacant(e) => {
let result = (calculation)(e.key());
e.insert(result)
}
}
}
}
fn main() {
let mut cacher1 = Cacher::new(|num: &u32| -> u32 {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(1));
*num
});
calculate_and_print(10, &mut cacher1);
calculate_and_print(20, &mut cacher1);
calculate_and_print(10, &mut cacher1);
let mut cacher2 = Cacher::new(|str: &&str| -> usize {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
str.len()
});
calculate_and_print("abc", &mut cacher2);
calculate_and_print("defghi", &mut cacher2);
calculate_and_print("abc", &mut cacher2);
}
fn calculate_and_print<P, R, T>(intensity: P, cacher: &mut Cacher<P, R, T>)
where
T: Fn(&P) -> R,
P: Eq + Hash,
R: Display,
{
println!("{}", cacher.value(intensity));
}
然后,您可以选择将其包装在执行克隆的另一个结构中:
struct ValueCacher<P, R, T>
where
T: Fn(&P) -> R,
P: Eq + Hash,
R: Clone,
{
cacher: Cacher<P, R, T>,
}
impl<P, R, T> ValueCacher<P, R, T>
where
T: Fn(&P) -> R,
P: Eq + Hash,
R: Clone,
{
fn new(calculation: T) -> Self {
Self {
cacher: Cacher::new(calculation),
}
}
fn value(&mut self, key: P) -> R {
self.cacher.value(key).clone()
}
}