bigywater.tech
Rust · 35

Cow<T> 的按需复制

「大多数时候不用改,偶尔需要改」是 Cow 的典型场景:不改就借用、改了才复制。这篇用真实的指针比较和分配计数验证它省了多少,以及 to_mut 在什么时候真正克隆。

10,000 → 1,000
10000 个字符串、10% 需要改时,String 版本与 Cow 版本的堆分配次数
24 字节
Cow 的大小,与 String 相同
to_mut()
第一次修改时才克隆

借用还是拥有

实测 · 本机执行输出
  源码:
      1 | use std::borrow::Cow;
      2 | fn strip_tabs(s: &str) -> Cow<'_, str> {
      3 |     if s.contains('\t') { Cow::Owned(s.replace('\t', " ")) } else { Cow::Borrowed(s) }
      4 | }
      5 | fn kind<T: ?Sized + ToOwned>(c: &Cow<'_, T>) -> &'static str { match c { Cow::Borrowed(_) => "Borrowed(没有分配)", Cow::Owned(_) => "Owned(分配了新 String)" } }
      6 | fn main() {
      7 |     let a = "no tabs here"; let b = "has\ttab";
      8 |     let ra = strip_tabs(a); let rb = strip_tabs(b);
      9 |     println!("{:<14} → {:?}  {}  指向原字符串:{}", a, ra, kind(&ra), std::ptr::eq(ra.as_ptr(), a.as_ptr()));
     10 |     println!("{:<14} → {:?}  {}", b, rb, kind(&rb));
     11 | }
  编译:成功
  运行(退出码 0):
    no tabs here   → "no tabs here"  Borrowed(没有分配)  指向原字符串:true
    has	tab        → "has tab"  Owned(分配了新 String)

省下多少分配

实测 · 本机执行输出
  源码:
      1 | use std::alloc::{GlobalAlloc, Layout, System};
      2 | use std::borrow::Cow;
      3 | use std::sync::atomic::{AtomicUsize, Ordering};
      4 | struct C; static AL: AtomicUsize = AtomicUsize::new(0);
      5 | unsafe impl GlobalAlloc for C {
      6 |     unsafe fn alloc(&self, l: Layout) -> *mut u8 { AL.fetch_add(1, Ordering::Relaxed); unsafe { System.alloc(l) } }
      7 |     unsafe fn dealloc(&self, p: *mut u8, l: Layout) { unsafe { System.dealloc(p, l) } }
      8 | }
      9 | #[global_allocator] static G: C = C;
     10 | fn always_string(s: &str) -> String { s.replace('\t', " ") }
     11 | fn cow(s: &str) -> Cow<'_, str> { if s.contains('\t') { Cow::Owned(s.replace('\t', " ")) } else { Cow::Borrowed(s) } }
     12 | fn main() {
     13 |     let inputs: Vec<String> = (0..10_000).map(|i| if i % 10 == 0 { format!("row{i}\tx") } else { format!("row{i} x") }).collect();
     14 |     let a0 = AL.load(Ordering::Relaxed);
     15 |     let mut n1 = 0usize; for s in &inputs { n1 += always_string(s).len(); }
     16 |     let a1 = AL.load(Ordering::Relaxed) - a0;
     17 |     let a0 = AL.load(Ordering::Relaxed);
     18 |     let mut n2 = 0usize; let mut owned = 0usize;
     19 |     for s in &inputs { let c = cow(s); if matches!(c, Cow::Owned(_)) { owned += 1; } n2 += c.len(); }
     20 |     let a2 = AL.load(Ordering::Relaxed) - a0;
     21 |     println!("每次都返回 String:堆分配 {} 次(总长度 {})", a1, n1);
     22 |     println!("返回 Cow<str>     :堆分配 {} 次(其中 Owned {} 个,总长度 {})", a2, owned, n2);
     23 | }
  编译:成功
  运行(退出码 0):
    每次都返回 String:堆分配 10000 次(总长度 88890)
    返回 Cow<str>     :堆分配 1000 次(其中 Owned 1000 个,总长度 88890)

分配次数用自定义 #[global_allocator] 计数,release 构建。

写时克隆

实测 · 本机执行输出
  源码:
      1 | use std::borrow::Cow;
      2 | fn main() {
      3 |     let original = String::from("hello");
      4 |     let mut c: Cow<str> = Cow::Borrowed(&original);
      5 |     println!("开始:{:?},是借用:{}", c, matches!(c, Cow::Borrowed(_)));
      6 |     c.to_mut().push_str(", world");            // 第一次修改:克隆,变成 Owned
      7 |     println!("to_mut().push_str 之后:{:?},是借用:{}", c, matches!(c, Cow::Borrowed(_)));
      8 |     c.to_mut().push('!');                       // 之后再修改不会再克隆(缓冲区可能因容量不足而移动,但不是 Cow 造成的)
      9 |     println!("再 push 一次:{:?};原字符串没有被改变:{:?}", c, original);
     10 |     let s: String = c.into_owned();
     11 |     println!("into_owned 得到 String:{s}");
     12 | }
  编译:成功
  运行(退出码 0):
    开始:"hello",是借用:true
    to_mut().push_str 之后:"hello, world",是借用:false
    再 push 一次:"hello, world!";原字符串没有被改变:"hello"
    into_owned 得到 String:hello, world!

大小

实测 · 本机执行输出
  编译:成功
  运行(退出码 0):
    String = 24,&str = 16,Cow<str> = 24
    Vec<u8> = 24,&[u8] = 16,Cow<[u8]> = 24

niche 的原理见Option<&T> 的空指针优化。

分步演示

Cow
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / rustc 1.98.1(brew)/ cargo 1.98.1;单个文件用 rustc 直接编译,需要依赖时用 cargo(经代理从 crates.io 下载);计时用 std::time::Instant,数字每次运行会有小幅抖动。分配次数为 release 构建的单次运行
    • 没有验证:Cow 对运行时间的影响(只数了分配)、Cow<'static, str> 的常见用法(错误信息)、Cow、to_mut 之后的 Cow 与 String 的互转开销、Cow 的解引用与 Deref 自动转换
    京ICP备2026067502号-1京公网安备11010502063487号