Cow在rust里的使用
越是了解Rust,你就会发现其中的言语方面设计的精细,比如这篇文章要讲的Cow。
标准库定义Cow为智能指针,目的是为了避免程序在内存操作上多余的复制拷贝数据。具体介绍可以看看标准库的文档。
定义
pub enum Cow<'a, B>
where
B: 'a + ToOwned + 'a + ?Sized,
{
Borrowed(&'a B),
Owned(<B as ToOwned>::Owned),
}
Cow的定义咋一看有点吓人,但可以细分下,其实也还好。
- Cow的泛型参数。包含一个生命周期
'a
和 类型B
。 B
的类型签名。可以看到B
必须实现了了两个trait:ToOwned
+?Sized
,还有一个生命周期'a
, 类型 B 的引用生命周期不小于'a
.ToOwned
。这个可以克隆数据到所有者。但有人肯定会问了,为什么不直接用Clone
。这二者还是有区别的,Clone 是从 &T 到 T,但ToOwned要强大些,可以从任意借用类型构建新类型实例。举个例子,
let s = "clone"; // str
let a: String = s.to_string(); // work
let b: String = s.clone(); // not work, str to String
let d: String = s.to_owned(); // work
可以看出不能从 str
克隆数据到 String
,因为不是 &T 到 T,但ToOwned可以。
?Sized
. 这个很好理解,允许在运行时决定类型的内存大小。- Cow里面包含两个变量值,要么借用(borrow),要么克隆数据给新的所有者(clone on write)。
例子
假如有个数组里包含了一些字符串,有些字符串有统一的前缀名,有些没有,也有可能全部都有或者全部没有。如果没有这个前缀名,那就插入这个前缀名。并且你不想修改输入的字符串,可能留作后用。
按照需求,函数签名和实现可能是这样的。
fn insert_prefix_clone<'a>(strs: impl IntoIterator<Item=&'a String>, prefix: &'a str) -> Vec<String> {
strs.into_iter().filter_map(|s|
match s.starts_with(prefix) {
true => Some(s.clone()),
false => Some(String::with_capacity(prefix.len() + s.len()) + prefix + s),
})
.collect::<Vec<String>>()
}
但这样做虽然能达到目的,但其实你的数组里可能包含着不少的已经拥有前缀名的字符串了,但函数也一股脑地拷贝了,这并没有做到rust里提倡的 zero cost。
所以这个时候,需要插入前缀名的字符串就拷贝,不需要就返回引用,这就节省了多余的内存开辟。
这就是Cow的用处了。所以用了Cow,这个函数的签名和实现大概如此:
fn insert_prefix_cow<'a, T>(strs: T, prefix: &'a str) -> Vec<Cow<'a, String>> where T: IntoIterator<Item=&'a String> {
strs.into_iter().filter_map(|s|
match s.starts_with(prefix) {
true => Some(Cow::Borrowed(s)),
false => Some(Cow::Owned(String::with_capacity(prefix.len() + s.len()) + prefix + s)),
}
).collect::<Vec<Cow<'a, String>>>()
}
用了Cow来封装数据,不需要插入前缀名的字符串,直接返回引用,二需要插入前缀名的字符串返回新的值。
那我们来细看下内存地址的有无变化。
let strs = vec!["row_rust".to_string(), "rust".to_string()];
let p = "row_";
let fixed = insert_prefix_cow(&strs, &p);
let s0 = &strs[0]; // 第一个元素已经有指定前缀名了
let f0 = &*f[0]; // Cow实现了Deref,所以可以直接解引用。
println!("source addr: {:?}", s0 as *const String); // 0x55aca68ac0
println!("cow addr: {:?}", f0 as *const String); // 0x55aca68ac0,地址相同
let s1 = &strs[1]; // 第二个元素插入了前缀名
let f1 = &*f[1];
println!("source addr: {:?}", s1 as *const String); // 0x55aca68ad8
println!("cow addr: {:?}", f1 as *const String); // 0x55aca68b88,地址已经发生了变化
可以看出,如果字符串包含了指定的前缀名,那么返回的数组里一部分字符串就指向原来的地址,也能看到地址确实没有变化,但如果插入了前缀名,那就意味着需要开辟新内存,也可以从结构看出,返回结果确实是指向了新的地址。
说了这么多,那我们来看下性能会如何?
Benchmark
Tips: 这个需要在nightly rust里面测试,我的测试环境是rust 1.38。
#![feature(test)]
extern crate test;
use test::Bencher;
// ...
#[bench]
fn test_cow(b: &mut Bencher) {
let mut c = vec!["cow_rust".to_string(); 1024];
let mut f = vec!["rust".to_string(); 1024];
c.append(&mut f);
let p = "cow_";
b.iter(|| insert_prefix_cow(&c, &p));
}
#[bench]
fn test_clone(b: &mut Bencher) {
let mut c = vec!["cow_rust".to_string(); 1024];
let mut f = vec!["rust".to_string(); 1024];
c.append(&mut f);
let p = "cow_";
b.iter(|| insert_prefix_clone(&c, &p));
}
看看测试结果。
running 2 tests
test test_clone ... bench: 218,206 ns/iter (+/- 3,479)
test test_cow ... bench: 178,341 ns/iter (+/- 1,487)
可以看出,使用了Cow的函数还是比较明显快于直接克隆的函数,而且,数据量越大,差距越明显。
结语
Cow 提供了更为精细的内存控制,对于数据能够尽可能地重复使用,这都反映了rust的精心设计。
更多的使用和介绍,参考标准库文档。
Enjoy it!