Rust in Everything
基于 bytes 1.12.1

reserve

主线位置:构建主线的最后一步,也是一圈的起点。split_to 把数据切走、freeze 交给共享主线之后,剩下的 BytesMut 要接着写,容量不够时就调 reserve。它决定这块分配是被原地收回、被扩大,还是被放弃:别的句柄还活着时,只能另分配一个新的 Vec,旧的那块留给共享主线上的 Bytes,等它们全部 drop。

flowchart TB
  entry["reserve(n)"] --> fast{"剩余容量够"}
  fast -->|是| ret[直接返回]
  fast -->|否| kind{KIND_VEC}
  kind -->|是| vok{"off ≥ len 且放得下"}
  vok -->|是| vback[挪回开头]
  vok -->|否| vres["Vec::reserve"]
  kind -->|否| uniq{只剩自己}
  uniq -->|是| fits{"整块放得下"}
  fits -->|是| setcap[只改 cap]
  fits -->|否| canmove{可以挪回开头}
  canmove -->|是| aback[挪回开头]
  canmove -->|否| grow["扩大 Vec"]
  uniq -->|否| fresh["新 Vec,回到 KIND_VEC"]

一块缓冲的一生 已经跑过 reserve 最常见的两种结果:所有切片都 drop 之后整块收回、指针回到开头;还有切片活着时另分配 8 KiB。这一页把 reserve_inner 的每个分支走一遍,再看「原始容量」、1.12.0 补的一句文档、1.11.1 修的整数溢出,以及共用这个函数的 try_reclaim 和 extend_from_within。data 字段的位怎么排(KIND_VEC / KIND_ARC、偏移、原始容量)在 data 字段的编码,split_to 怎么把 KIND_VEC 提升成 KIND_ARC 在 split 与 freeze。

入口:快路径和 reserve_inner

reserve:

    #[inline]
    pub fn reserve(&mut self, additional: usize) {
        let len = self.len();
        let rem = self.capacity() - len;

        if additional <= rem {
            // The handle can already store at least `additional` more bytes, so
            // there is no further work needed to be done.
            return;
        }

        // will always succeed
        let _ = self.reserve_inner(additional, true);
    }

    // In separate function to allow the short-circuits in `reserve` and `try_reclaim` to
    // be inline-able. Significantly helps performance. Returns false if it did not succeed.
    fn reserve_inner(&mut self, additional: usize, allocate: bool) -> bool {

additional 是「在 len 之后还要能再放多少字节」,不是新的总容量。剩余容量 cap - len 已经够了就直接返回,在循环里反复调用 reserve 的代码,绝大多数调用在这里就返回了。慢路径单独放进 reserve_inner,注释说是为了让 reserve 和 try_reclaim 的快路径能被内联。第二个参数 allocate 是给 try_reclaim 用的,reserve 总是传 true,后面单独讲。

reserve_inner 先看 data 的最低位:KIND_VEC 是一个独占的 Vec,前面可能被 advance 吃掉了 off 个字节;KIND_ARC 是已经 split 过、和别的句柄共用一个 Shared 的缓冲。两种形态各有各的分支。

一个程序跑遍所有分支

下面的程序给每个分支造一个场景,各跑一次 reserve 和一次 try_reclaim(每次都重新造),打印前后的 ptr、len、cap 和分配次数。ptr 落在最初那块分配里就打印相对起点 base 的偏移,否则打印 elsewhere。计数分配器把 alloc、realloc、dealloc 分开数,括号里是新申请的字节数(realloc 记新的大小):Vec 扩容走的是 realloc,分开数才看得出「扩大」和「另起一块」的区别。

bytes = "=1.12.1"

common.rs:

use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering::Relaxed};

use bytes::BytesMut;

// 计数分配器:分别数 alloc、realloc、dealloc,真正的分配交给 System
struct Counting;
static ALLOCS: AtomicUsize = AtomicUsize::new(0);
static REALLOCS: AtomicUsize = AtomicUsize::new(0);
static FREES: AtomicUsize = AtomicUsize::new(0);
// 新申请的字节数:alloc 记 size,realloc 记新的 size
static BYTES: AtomicUsize = AtomicUsize::new(0);
// 不小于 1 KiB 的 alloc:在下面的实验里就是「新缓冲」
static BIG_ALLOCS: AtomicUsize = AtomicUsize::new(0);

unsafe impl GlobalAlloc for Counting {
  unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
    ALLOCS.fetch_add(1, Relaxed);
    BYTES.fetch_add(layout.size(), Relaxed);
    if layout.size() >= 1024 {
      BIG_ALLOCS.fetch_add(1, Relaxed);
    }
    unsafe { System.alloc(layout) }
  }
  unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
    REALLOCS.fetch_add(1, Relaxed);
    BYTES.fetch_add(new_size, Relaxed);
    unsafe { System.realloc(ptr, layout, new_size) }
  }
  unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
    FREES.fetch_add(1, Relaxed);
    unsafe { System.dealloc(ptr, layout) }
  }
}

#[global_allocator]
static GLOBAL: Counting = Counting;

#[derive(Clone, Copy)]
pub struct Snap {
  pub allocs: usize,
  pub reallocs: usize,
  pub frees: usize,
  pub bytes: usize,
  pub big: usize,
}

pub fn snap() -> Snap {
  Snap {
    allocs: ALLOCS.load(Relaxed),
    reallocs: REALLOCS.load(Relaxed),
    frees: FREES.load(Relaxed),
    bytes: BYTES.load(Relaxed),
    big: BIG_ALLOCS.load(Relaxed),
  }
}

pub fn delta(before: Snap) -> Snap {
  let now = snap();
  Snap {
    allocs: now.allocs - before.allocs,
    reallocs: now.reallocs - before.reallocs,
    frees: now.frees - before.frees,
    bytes: now.bytes - before.bytes,
    big: now.big - before.big,
  }
}

impl std::fmt::Display for Snap {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    write!(f, "alloc+{} realloc+{} ({}B) free+{}", self.allocs, self.reallocs, self.bytes, self.frees)
  }
}

// 指针落在最初那块分配 [base, base+size) 里就打印偏移,否则是别处
pub fn loc(p: *const u8, base: usize, size: usize) -> String {
  let p = p as usize;
  if base <= p && p < base + size { format!("base+{}", p - base) } else { "elsewhere".to_string() }
}

pub fn state(b: &BytesMut, base: usize, size: usize) -> String {
  format!("ptr={} len={} cap={}", loc(b.as_ptr(), base, size), b.len(), b.capacity())
}

branches.rs:

use bytes::{Buf, BufMut, Bytes, BytesMut};

#[path = "../common.rs"]
mod common;
use common::*;

// 一个场景:造好 BytesMut(可能还有别的句柄活着),返回它、活着的句柄、最初那块分配的起点和大小
struct Case {
  buf: BytesMut,
  keep: Option<Bytes>,
  base: usize,
  size: usize,
}

fn filled(cap: usize, n: usize) -> (BytesMut, usize) {
  let mut buf = BytesMut::with_capacity(cap);
  let base = buf.as_ptr() as usize;
  buf.put_bytes(b'x', n);
  (buf, base)
}

fn vec_copy_back() -> Case {
  let (mut buf, base) = filled(16, 8);
  buf.advance(6); // KIND_VEC,off=6,len=2
  Case { buf, keep: None, base, size: 16 }
}

fn vec_off_lt_len() -> Case {
  let (mut buf, base) = filled(16, 12);
  buf.advance(4); // KIND_VEC,off=4,len=8
  Case { buf, keep: None, base, size: 16 }
}

fn vec_full() -> Case {
  let (buf, base) = filled(100, 100); // KIND_VEC,off=0,满
  Case { buf, keep: None, base, size: 100 }
}

fn arc_unique(cap: usize, n: usize, head: usize) -> Case {
  let (mut buf, base) = filled(cap, n);
  drop(buf.split_to(head)); // 提升成 KIND_ARC,再把另一半扔掉:只剩自己
  Case { buf, keep: None, base, size: cap }
}

fn arc_shared(make: fn() -> BytesMut) -> Case {
  let mut buf = make();
  let base = buf.as_ptr() as usize;
  let size = buf.capacity();
  buf.put_bytes(b'x', size);
  let head = buf.split_to(size - 192).freeze(); // head 活着,buf 剩 192 字节
  Case { buf, keep: Some(head), base, size }
}

fn cap8192() -> BytesMut {
  BytesMut::with_capacity(8192)
}

fn cap0_reserve8192() -> BytesMut {
  let mut b = BytesMut::with_capacity(0);
  b.reserve(8192);
  b
}

fn run(name: &str, make: &dyn Fn() -> Case, additional: usize) {
  println!("== {name}, additional={additional}");
  for try_only in [false, true] {
    let Case { mut buf, keep, base, size } = make();
    let before = state(&buf, base, size);
    let s = snap();
    let ok = if try_only { buf.try_reclaim(additional) } else { buf.reserve(additional); true };
    let d = delta(s);
    let op = if try_only { "try_reclaim" } else { "reserve" };
    println!("  {op:<11} -> {ok:<5} {before}  =>  {}  {d}", state(&buf, base, size));
    drop(keep);
  }
}

fn main() {
  run("KIND_VEC 挪回开头", &vec_copy_back, 12);
  run("KIND_VEC off<len", &vec_off_lt_len, 8);
  run("KIND_VEC 满", &vec_full, 1);
  run("KIND_ARC 唯一 挪回", &|| arc_unique(64, 16, 12), 56);
  run("KIND_ARC 唯一 扩大", &|| arc_unique(64, 40, 10), 60);
  run("KIND_ARC 唯一 扩大", &|| arc_unique(64, 40, 10), 100);
  run("KIND_ARC 共享 cap8192", &|| arc_shared(cap8192), 100);
  run("KIND_ARC 共享 cap8192", &|| arc_shared(cap8192), 9000);
  run("KIND_ARC 共享 cap0+reserve", &|| arc_shared(cap0_reserve8192), 100);
}

M2 Max、rustc 1.99.0,cargo run --release --bin branches,跑 3 次,输出相同:

== KIND_VEC 挪回开头, additional=12
  reserve     -> true  ptr=base+6 len=2 cap=10  =>  ptr=base+0 len=2 cap=16  alloc+0 realloc+0 (0B) free+0
  try_reclaim -> true  ptr=base+6 len=2 cap=10  =>  ptr=base+0 len=2 cap=16  alloc+0 realloc+0 (0B) free+0
== KIND_VEC off<len, additional=8
  reserve     -> true  ptr=base+4 len=8 cap=12  =>  ptr=elsewhere len=8 cap=28  alloc+0 realloc+1 (32B) free+0
  try_reclaim -> false ptr=base+4 len=8 cap=12  =>  ptr=base+4 len=8 cap=12  alloc+0 realloc+0 (0B) free+0
== KIND_VEC 满, additional=1
  reserve     -> true  ptr=base+0 len=100 cap=100  =>  ptr=elsewhere len=100 cap=200  alloc+0 realloc+1 (200B) free+0
  try_reclaim -> false ptr=base+0 len=100 cap=100  =>  ptr=base+0 len=100 cap=100  alloc+0 realloc+0 (0B) free+0
== KIND_ARC 唯一 挪回, additional=56
  reserve     -> true  ptr=base+12 len=4 cap=52  =>  ptr=base+0 len=4 cap=64  alloc+0 realloc+0 (0B) free+0
  try_reclaim -> true  ptr=base+12 len=4 cap=52  =>  ptr=base+0 len=4 cap=64  alloc+0 realloc+0 (0B) free+0
== KIND_ARC 唯一 扩大, additional=60
  reserve     -> true  ptr=base+10 len=30 cap=54  =>  ptr=elsewhere len=30 cap=118  alloc+0 realloc+1 (128B) free+0
  try_reclaim -> false ptr=base+10 len=30 cap=54  =>  ptr=base+10 len=30 cap=54  alloc+0 realloc+0 (0B) free+0
== KIND_ARC 唯一 扩大, additional=100
  reserve     -> true  ptr=base+10 len=30 cap=54  =>  ptr=elsewhere len=30 cap=130  alloc+0 realloc+1 (140B) free+0
  try_reclaim -> false ptr=base+10 len=30 cap=54  =>  ptr=base+10 len=30 cap=54  alloc+0 realloc+0 (0B) free+0
== KIND_ARC 共享 cap8192, additional=100
  reserve     -> true  ptr=base+8000 len=192 cap=192  =>  ptr=elsewhere len=192 cap=8192  alloc+1 realloc+0 (8192B) free+0
  try_reclaim -> false ptr=base+8000 len=192 cap=192  =>  ptr=base+8000 len=192 cap=192  alloc+0 realloc+0 (0B) free+0
== KIND_ARC 共享 cap8192, additional=9000
  reserve     -> true  ptr=base+8000 len=192 cap=192  =>  ptr=elsewhere len=192 cap=9192  alloc+1 realloc+0 (9192B) free+0
  try_reclaim -> false ptr=base+8000 len=192 cap=192  =>  ptr=base+8000 len=192 cap=192  alloc+0 realloc+0 (0B) free+0
== KIND_ARC 共享 cap0+reserve, additional=100
  reserve     -> true  ptr=base+8000 len=192 cap=192  =>  ptr=elsewhere len=192 cap=292  alloc+1 realloc+0 (292B) free+0
  try_reclaim -> false ptr=base+8000 len=192 cap=192  =>  ptr=base+8000 len=192 cap=192  alloc+0 realloc+0 (0B) free+0

下面几节逐个分支对照源码解释这些数字。KIND_ARC 唯一时还有一种「只改 cap」的结果,留作本页的动脑筋。

KIND_VEC:挪回开头,或者交给 Vec

KIND_VEC 的 BytesMut 还是一个 Vec,只是 ptr 可能被 advance 往后挪了 off 个字节(off 记在 data 的高位,见 data 字段的编码)。前面这 off 个字节已经被读走,是可以回收的空间。reserve_inner 的第一段:

        if kind == KIND_VEC {
            // ...
            unsafe {
                let off = self.get_vec_pos();

                // Only reuse space if we can satisfy the requested additional space.
                //
                // Also check if the value of `off` suggests that enough bytes
                // have been read to account for the overhead of shifting all
                // the data (in an amortized analysis).
                // Hence the condition `off >= self.len()`.
                //
                // This condition also already implies that the buffer is going
                // to be (at least) half-empty in the end; so we do not break
                // the (amortized) runtime with future resizes of the underlying
                // `Vec`.
                //
                // [For more details check issue #524, and PR #525.]
                if self.capacity() - self.len() + off >= additional && off >= self.len() {
                    // There's enough space, and it's not too much overhead:
                    // reuse the space!
                    //
                    // Just move the pointer back to the start after copying
                    // data back.
                    let base_ptr = self.ptr.as_ptr().sub(off);
                    // Since `off >= self.len()`, the two regions don't overlap.
                    ptr::copy_nonoverlapping(self.ptr.as_ptr(), base_ptr, self.len);
                    self.ptr = vptr(base_ptr);
                    self.set_vec_pos(0);

                    // Length stays constant, but since we moved backwards we
                    // can gain capacity back.
                    self.cap += off;
                } else {
                    if !allocate {
                        return false;
                    }
                    // Not enough space, or reusing might be too much overhead:
                    // allocate more space!
                    let mut v =
                        ManuallyDrop::new(rebuild_vec(self.ptr.as_ptr(), self.len, self.cap, off));
                    v.reserve(additional);

                    // Update the info
                    self.ptr = vptr(v.as_mut_ptr().add(off));
                    self.cap = v.capacity() - off;
                    debug_assert_eq!(self.len, v.len() - off);
                }

                return true;
            }
        }

挪回开头要同时满足两个条件:

条件意思
cap - len + off >= additional把前面 off 个字节收回来之后,剩余空间够 additional
off >= len已经读走的字节不少于要挪的字节

第二个条件是为了摊还:挪一次要拷贝 len 个字节,off >= len 保证这次拷贝的量不超过此前读走的量,把拷贝的代价摊到那些读上,整体仍是线性的。它还让源区间 [off, off+len) 和目标区间 [0, len) 不重叠,所以能用 copy_nonoverlapping。

输出的第一组就是这条路:16 字节的缓冲写了 8 个、读走 6 个,剩 "gh",off=6、len=2、cap=10。reserve(12) 时 10 − 2 + 6 = 14 ≥ 12,6 ≥ 2,于是把 2 个字节拷回开头,ptr 回到 base+0,cap 加回 6 变成 16,没有分配。

条件不满足就走 else:rebuild_vec 把 ptr - off、len + off、cap + off 拼回原来那个 Vec,交给 Vec::reserve(additional)。这里没有自己算新容量,用的是 Vec 的摊还扩容:至少翻倍。第二组 off=4、len=8:空间算上 off 刚好够(12 − 8 + 4 = 8),但 4 < 8,挪回开头不划算,于是拼回的 Vec 是 len=12、cap=16,reserve(8) 需要 20,翻倍是 32,取 32,realloc 一次 32 字节。新的 cap 是 32 − 4 = 28:前面那 4 个已经读走的字节被原样带进了新的分配,off 不变。第三组没有 off、缓冲是满的,reserve(1) 让 Vec 从 100 翻倍到 200。

📝NOTE
冷知识:挪回开头曾经没有 off >= len 这个条件

1.2.0 之前,KIND_VEC 分支只要「收回 off 之后放得下」就挪,条件只有前一半。issue #524(「Bad “optimization” of BytesMut re-allocations」)指出这会毁掉摊还复杂度:一个装满 1 000 000 字节的 BytesMut,交替做 10 000 次 get_u8 和 put_u8,每次 put_u8 都发现前面空出了 1 个字节、刚好够,于是把一百万字节整体挪一遍。issue 里的实测是「633.147ms - 10'000 × get+put (each moving all the data)」,而重新分配一次之后同样的 10 000 次只要「223.719µs」。

修复先是 PR #525,后来由 PR #555(「Fix amortized asymptotics of BytesMut」,说明是「Replacement for #525 as it has been stale for really long」)合入:加上 off >= self.len(),把 ptr::copy 换成 copy_nonoverlapping,并写下上面那段注释。reserve 的文档同时改成现在的说法:「as of now, the length of the data being shifted needs to be at least as large as the distance that it's shifted by」。CHANGELOG 记在 1.2.0:「Fix amortized asymptotics of BytesMut (#555)」。

KIND_ARC 且唯一:整块收回

KIND_ARC 时 data 指向 bytes_mut.rs 自己的 Shared,里面是整个原始 Vec、原始容量和引用计数。先算出需要的容量,再看计数:

        debug_assert_eq!(kind, KIND_ARC);
        let shared: *mut Shared = self.data;

        // Reserving involves abandoning the currently shared buffer and
        // allocating a new vector with the requested capacity.
        //
        // Compute the new capacity
        let mut new_cap = match len.checked_add(additional) {
            Some(new_cap) => new_cap,
            None if !allocate => return false,
            None => panic!("overflow"),
        };

        unsafe {
            // First, try to reclaim the buffer. This is possible if the current
            // handle is the only outstanding handle pointing to the buffer.
            if (*shared).is_unique() {
                // This is the only handle to the buffer. It can be reclaimed.
                // However, before doing the work of copying data, check to make
                // sure that the vector has enough capacity.
                let v = &mut (*shared).vec;

                let v_capacity = v.capacity();
                let ptr = v.as_mut_ptr();

                let offset = self.ptr.as_ptr().offset_from(ptr) as usize;

                let new_cap_plus_offset = match new_cap.checked_add(offset) {
                    Some(new_cap_plus_offset) => new_cap_plus_offset,
                    None if !allocate => return false,
                    None => panic!("overflow"),
                };

                // Compare the condition in the `kind == KIND_VEC` case above
                // for more details.
                if v_capacity >= new_cap_plus_offset {
                    self.cap = new_cap;
                    // no copy is necessary
                } else if v_capacity >= new_cap && offset >= len {
                    // The capacity is sufficient, and copying is not too much
                    // overhead: reclaim the buffer!

                    // `offset >= len` means: no overlap
                    ptr::copy_nonoverlapping(self.ptr.as_ptr(), ptr, len);

                    self.ptr = vptr(ptr);
                    self.cap = v.capacity();
                } else {
                    // ...

new_cap = len + additional 是以 BytesMut 自己的起点算的;offset 是自己的 ptr 离整块起点有多远。Shared::is_unique 用 Acquire 读计数、判断是否为 1:别的句柄都 drop 了,整块 Vec 就归自己,可以随便用。于是有三种结果:

条件结果分配
v_capacity >= new_cap + offset不动数据,cap 设成 new_cap无
v_capacity >= new_cap 且 offset >= len把 len 个字节拷回整块开头,cap 设成整块容量无
其他扩大整块 Vecrealloc

第二行的 offset >= len 和 KIND_VEC 分支是同一个摊还条件。输出里「KIND_ARC 唯一 挪回」那组:64 字节的缓冲写 16 个、split_to(12) 后把前一半 drop,自己是 base+12、len=4、cap=52。reserve(56) 时 new_cap = 60,60 + 12 = 72 > 64,第一行不成立;64 ≥ 60 且 12 ≥ 4,拷回开头,cap 变成整块的 64。

第三种结果:

                } else {
                    if !allocate {
                        return false;
                    }

                    // new_cap is calculated in terms of `BytesMut`, not the underlying
                    // `Vec`, so it does not take the offset into account.
                    //
                    // Thus we have to manually add it here.
                    new_cap = new_cap_plus_offset;

                    // The vector capacity is not sufficient. The reserve request is
                    // asking for more than the initial buffer capacity. Allocate more
                    // than requested if `new_cap` is not much bigger than the current
                    // capacity.
                    //
                    // There are some situations, using `reserve_exact` that the
                    // buffer capacity could be below `original_capacity`, so do a
                    // check.
                    let double = v.capacity().checked_shl(1).unwrap_or(new_cap);

                    new_cap = cmp::max(double, new_cap);

                    // No space - allocate more
                    //
                    // The length field of `Shared::vec` is not used by the `BytesMut`;
                    // instead we use the `len` field in the `BytesMut` itself. However,
                    // when calling `reserve`, it doesn't guarantee that data stored in
                    // the unused capacity of the vector is copied over to the new
                    // allocation, so we need to ensure that we don't have any data we
                    // care about in the unused capacity before calling `reserve`.
                    debug_assert!(offset + len <= v.capacity());
                    v.set_len(offset + len);
                    v.reserve(new_cap - v.len());

                    // Update the info
                    self.ptr = vptr(v.as_mut_ptr().add(offset));
                    self.cap = v.capacity() - offset;
                }

                return true;
            }
        }

目标容量是 max(2 × 整块容量, new_cap + offset),以整块 Vec 为单位。Shared::vec 的 len 平时不维护(BytesMut 用自己的 len),所以扩容前先 set_len(offset + len),告诉 Vec 哪些字节是要保留的,再 v.reserve(new_cap - v.len())。和 KIND_VEC 分支一样,前面 offset 个已经没用的字节也被带进了新的分配,offset 不变。

输出里两组「扩大」都是 64 字节的缓冲、offset=10、len=30:

additionalnew_cap + offset2 × 64取realloc 大小新 cap(减去 offset)
6090 + 10 = 100128128128B118
100130 + 10 = 140128140140B130

两个细节。checked_shl(1) 只在移位位数不小于 usize 的位数时返回 None,这里移 1 位,它永远是 Some,起不到溢出检查的作用;真正防溢出的是前面两个 checked_add。注释里提到的 reserve_exact 在 BytesMut 上并不存在。

❗IMPORTANT
动脑筋:唯一时 reserve 之后的容量是多少?

平台 M2 Max、rustc 1.99.0,common.rs 同上。64 字节的缓冲写 16 个字节,split_off(8) 后把后一半 drop,只剩前 8 个字节这一个句柄(cap 被截成 8)。接着 reserve(32),再 reserve(56)。两次之后的 cap 各是多少?有没有分配?

use bytes::{BufMut, BytesMut};

#[path = "../common.rs"]
mod common;
use common::*;

fn main() {
  let mut buf = BytesMut::with_capacity(64);
  let base = buf.as_ptr() as usize;
  buf.put_bytes(b'x', 16);
  drop(buf.split_off(8)); // 只剩前 8 字节这一个句柄
  for additional in [32, 56] {
    let s = snap();
    buf.reserve(additional);
    let d = delta(s);
    println!("reserve({additional}) -> {}  {d}", state(&buf, base, 64));
  }
}

KIND_ARC 且共享:另起一个 Vec

计数不是 1,说明还有别的 BytesMut 或 Bytes 指着这块内存。它们的数据不能动,自己也不能写进它们的区间,只能放弃这块分配:

        if !allocate {
            return false;
        }

        let original_capacity_repr = unsafe { (*shared).original_capacity_repr };
        let original_capacity = original_capacity_from_repr(original_capacity_repr);

        new_cap = cmp::max(new_cap, original_capacity);

        // Create a new vector to store the data
        let mut v = ManuallyDrop::new(Vec::with_capacity(new_cap));

        // Copy the bytes
        v.extend_from_slice(self.as_ref());

        // Release the shared handle. This must be done *after* the bytes are
        // copied.
        unsafe { release_shared(shared) };

        // Update self
        let data = (original_capacity_repr << ORIGINAL_CAPACITY_OFFSET) | KIND_VEC;
        self.data = invalid_ptr(data);
        self.ptr = vptr(v.as_mut_ptr());
        self.cap = v.capacity();
        debug_assert_eq!(self.len, v.len());
        true
    }
  1. 新容量是 max(len + additional, 原始容量),用 Vec::with_capacity 精确分配,不翻倍。
  2. 只拷贝 len 个有效字节。
  3. 拷完才 release_shared:先放手的话,别的句柄可能恰好在这时 drop,把整块释放掉,拷贝就读到了已释放的内存。
  4. data 重新编码成 KIND_VEC,偏移为 0,原始容量的那几位原样带过来。这个 BytesMut 回到了「独占一个 Vec」的形态,下次 split_to 时会再提升一次(再分配一个 Shared)。

输出最后三组都是这条路:缓冲写满,切走前 8000 字节冻结成 head 并让它活着,剩下 192 字节。with_capacity(8192) 的缓冲 reserve(100) 得到 8192(原始容量胜出),reserve(9000) 得到 9192(需求胜出);with_capacity(0) 再 reserve(8192) 的缓冲同样 reserve(100),只得到 292。旧缓冲没有释放(free+0),它现在只为 head 活着。hyper 里这意味着什么,见 一块缓冲的一生 的动脑筋。

原始容量

上面最后两组的区别来自「原始容量」。它只在 from_vec 里记录一次,用的是创建时 Vec 的容量:

        let original_capacity_repr = original_capacity_to_repr(cap);
        let data = (original_capacity_repr << ORIGINAL_CAPACITY_OFFSET) | KIND_VEC;

只有 3 位,所以存的是一个对数。original_capacity_to_repr 和 original_capacity_from_repr:

fn original_capacity_to_repr(cap: usize) -> usize {
    let width = PTR_WIDTH - ((cap >> MIN_ORIGINAL_CAPACITY_WIDTH).leading_zeros() as usize);
    cmp::min(
        width,
        MAX_ORIGINAL_CAPACITY_WIDTH - MIN_ORIGINAL_CAPACITY_WIDTH,
    )
}

fn original_capacity_from_repr(repr: usize) -> usize {
    if repr == 0 {
        return 0;
    }

    1 << (repr + (MIN_ORIGINAL_CAPACITY_WIDTH - 1))
}

MIN_ORIGINAL_CAPACITY_WIDTH 是 10、MAX_ORIGINAL_CAPACITY_WIDTH 是 17:不到 1 KiB 记成 0(不起作用),其余向下取到 2 的幂,最多 64 KiB。位的布局在 data 字段的编码,这里只看它对 reserve 的影响。用共享分支量一下:

use bytes::{BufMut, BytesMut};

#[path = "../common.rs"]
mod common;
use common::*;

// 用 with_capacity(c) 建缓冲(c=0 时再 reserve(8192)),写满,
// 切走前面一段并让它活着,剩 10 字节时 reserve(1):走共享分支,新容量 = max(11, 原始容量)
fn after_shared_reserve(c: usize) -> (usize, Snap) {
  let mut buf = BytesMut::with_capacity(c);
  if c == 0 {
    buf.reserve(8192);
  }
  let cap = buf.capacity();
  buf.put_bytes(b'x', cap);
  let head = buf.split_to(cap - 10).freeze();
  let s = snap();
  buf.reserve(1);
  let d = delta(s);
  drop(head);
  (buf.capacity(), d)
}

fn main() {
  for c in [0usize, 1000, 1023, 1024, 5000, 8192, 65536, 1 << 20] {
    let (cap, d) = after_shared_reserve(c);
    println!("with_capacity({c:>7}) -> reserve(1) cap={cap:<6} {d}");
  }
}

跑 3 次,输出相同:

with_capacity(      0) -> reserve(1) cap=11     alloc+1 realloc+0 (11B) free+0
with_capacity(   1000) -> reserve(1) cap=11     alloc+1 realloc+0 (11B) free+0
with_capacity(   1023) -> reserve(1) cap=11     alloc+1 realloc+0 (11B) free+0
with_capacity(   1024) -> reserve(1) cap=1024   alloc+1 realloc+0 (1024B) free+0
with_capacity(   5000) -> reserve(1) cap=4096   alloc+1 realloc+0 (4096B) free+0
with_capacity(   8192) -> reserve(1) cap=8192   alloc+1 realloc+0 (8192B) free+0
with_capacity(  65536) -> reserve(1) cap=65536  alloc+1 realloc+0 (65536B) free+0
with_capacity(1048576) -> reserve(1) cap=65536  alloc+1 realloc+0 (65536B) free+0
创建时的容量记下的原始容量
0、1000、10230
10241024
50004096
81928192
65536、1 MiB65536

第一行是 with_capacity(0) 之后 reserve(8192) 的缓冲:它此刻的容量明明是 8192,原始容量却是 0。原因是原始容量只在 from_vec 里写;之后 KIND_VEC 分支的 Vec::reserve 只改 ptr 和 cap,不碰 data 的那几位;共享分支也是把旧的 original_capacity_repr 原样搬过去。一个以容量 0 出生的缓冲,原始容量永远是 0。hyper 的读缓冲正是这样出生的:Buffered::new 里是 BytesMut::with_capacity(0)。

原始容量的用意是「这个缓冲当初要了多大,被迫换新块时就至少给这么大」,避免只按眼前的 len + additional 分配一个很小的块、紧接着又要扩容。对 hyper 来说它丢了也没关系:poll_read_from_io 每次 reserve(next),默认的 Adaptive 策略里 next 从 INIT_BUFFER_SIZE(8192)起步,record 往下调时也不低于它(只有读缓冲里已经攒到接近 max_buf_size 时,poll_read_from_io 才会把它截短),所以 len + next 本来就不小于 8192。每次只 reserve 一小段的代码才会吃亏。读缓冲的读策略见 hyper 卷的 读写缓冲。

实测:一个类 hyper 的读循环

模拟一个连接:每轮先 reserve,再「从 socket 读」4 KiB(put_slice),每攒够 1000 字节就 split_to(1000).freeze() 切出一帧。大多数帧切出来马上用完;每 every 帧里有一帧被上层留 hold 轮。跑 10 000 轮,比较两种缓冲:with_capacity(8192),以及 hyper 的 with_capacity(0) 加 reserve(8192);每轮 reserve 的量分 4096 和 8192 两种。

use std::collections::VecDeque;

use bytes::{BufMut, Bytes, BytesMut};

#[path = "../common.rs"]
mod common;
use common::*;

const ITERS: usize = 10_000;
const READ: usize = 4096;
const FRAME: usize = 1000;

// 每 every 帧里有一帧被上层留 hold 轮,其余帧切出来马上用完
fn run(label: &str, mut buf: BytesMut, reserve: usize, every: usize, hold: usize) {
  let chunk = [b'x'; READ];
  let mut alive: VecDeque<(usize, Bytes)> = VecDeque::with_capacity(ITERS);
  let mut n = 0;
  let s = snap();
  for i in 0..ITERS {
    while alive.front().is_some_and(|(until, _)| *until <= i) {
      alive.pop_front();
    }
    buf.reserve(reserve);
    buf.put_slice(&chunk);
    while buf.len() >= FRAME {
      let frame = buf.split_to(FRAME).freeze();
      if n % every == 0 {
        alive.push_back((i + hold, frame));
      }
      n += 1;
    }
  }
  let d = delta(s);
  println!(
    "{label:<34} reserve({reserve}) 1/{every} held {hold}: allocs={:<5} buffers={:<5} bytes={:<9} reallocs={}",
    d.allocs, d.big, d.bytes, d.reallocs
  );
}

fn cap8192() -> BytesMut {
  BytesMut::with_capacity(8192)
}

fn cap0_reserve8192() -> BytesMut {
  let mut b = BytesMut::with_capacity(0);
  b.reserve(8192);
  b
}

fn main() {
  for reserve in [READ, 8192] {
    for (every, hold) in [(1, 2), (16, 32), (64, 100), (256, 1000)] {
      run("with_capacity(8192)", cap8192(), reserve, every, hold);
      run("with_capacity(0) + reserve(8192)", cap0_reserve8192(), reserve, every, hold);
    }
  }
}

allocs 是循环里所有的 alloc,buffers 是其中不小于 1 KiB 的(新缓冲),差值是第一次 split_to 时的 40 字节 Shared;bytes 包括 realloc 的新大小。跑 3 次,输出逐字节相同(计数不依赖时序):

with_capacity(8192)                reserve(4096) 1/1 held 2: allocs=19839 buffers=9919  bytes=81653248  reallocs=0
with_capacity(0) + reserve(8192)   reserve(4096) 1/1 held 2: allocs=19997 buffers=9998  bytes=46311672  reallocs=0
with_capacity(8192)                reserve(4096) 1/16 held 32: allocs=5121  buffers=2560  bytes=21073960  reallocs=0
with_capacity(0) + reserve(8192)   reserve(4096) 1/16 held 32: allocs=5121  buffers=2560  bytes=32865456  reallocs=2318
with_capacity(8192)                reserve(4096) 1/64 held 100: allocs=1281  buffers=640   bytes=5268520   reallocs=0
with_capacity(0) + reserve(8192)   reserve(4096) 1/64 held 100: allocs=1281  buffers=640   bytes=8543048   reallocs=640
with_capacity(8192)                reserve(4096) 1/256 held 1000: allocs=321   buffers=160   bytes=1317160   reallocs=0
with_capacity(0) + reserve(8192)   reserve(4096) 1/256 held 1000: allocs=321   buffers=160   bytes=2030120   reallocs=160
with_capacity(8192)                reserve(8192) 1/1 held 2: allocs=19999 buffers=9999  bytes=87271808  reallocs=0
with_capacity(0) + reserve(8192)   reserve(8192) 1/1 held 2: allocs=19999 buffers=9999  bytes=87271808  reallocs=0
with_capacity(8192)                reserve(8192) 1/16 held 32: allocs=5121  buffers=2560  bytes=58328360  reallocs=2080
with_capacity(0) + reserve(8192)   reserve(8192) 1/16 held 32: allocs=5121  buffers=2560  bytes=58328360  reallocs=2080
with_capacity(8192)                reserve(8192) 1/64 held 100: allocs=1281  buffers=640   bytes=16453160  reallocs=640
with_capacity(0) + reserve(8192)   reserve(8192) 1/64 held 100: allocs=1281  buffers=640   bytes=16453160  reallocs=640
with_capacity(8192)                reserve(8192) 1/256 held 1000: allocs=321   buffers=160   bytes=3973160   reallocs=160
with_capacity(0) + reserve(8192)   reserve(8192) 1/256 held 1000: allocs=321   buffers=160   bytes=3973160   reallocs=160

读这张表:

  • reserve(8192) 的八行两两相同。 这是 hyper 的做法:每次要的量本身就不小于 8192,max(len + 8192, 原始容量) 里原始容量从不胜出,丢没丢它没有区别。
  • 新缓冲的个数只取决于有没有帧活着。 被留下的帧数和新缓冲数一样:10 000 轮共切出 40 960 帧,1/16 被留下是 2560 帧,新缓冲也是 2560 个。reserve 时只要缓冲里还有一帧活着,就只能走共享分支另起一块。两种缓冲这一项完全一样,原始容量改变的是新缓冲的大小。
  • reserve(4096)、只偶尔留帧时,原始容量省掉了 realloc。 原始容量为 0 时,共享分支给的新缓冲正好是 len + 4096(len 是上一轮剩下不到 1000 字节的零头,记作 L0),读一次就满。等留下的帧 drop、下一轮 reserve(4096) 走唯一分支时,零头变成了 L1:整块容量 L0 + 4096,需要 L1 + 4096,按源码推算,只有 L1 ≤ L0 时才能挪回开头,否则只能 realloc 扩大。零头每轮多 96 字节(4096 mod 1000,满 1000 时回绕),大多数时候 L1 > L0,于是多出 2318 次 realloc。原始容量为 8192 时新缓冲是 8192,挪回开头总放得下,realloc 为 0,字节数也少三分之一以上。
  • 每一帧都被留两轮时,原始容量反而更费内存。 每轮 reserve 时上一轮的帧都还活着,每轮都要另起新缓冲。原始容量让每个新缓冲都是 8192,可每轮只写 4096,另一半几乎用不上:新缓冲只少了几十个(9919 对 9998),总字节数却是 81.6 MB 对 46.3 MB。

原始容量是一个启发式:它押注「缓冲很快会被收回、复用」。押中了省掉扩容,押错了每块新缓冲都多占一倍。

「不保留未使用的容量」(#808)

1.12.0 给 reserve 和 try_reclaim 的文档各加了一句:

    /// This method does not preserve data stored in the unused capacity.

「未使用的容量」是 [len, cap) 这一段,也就是 spare_capacity_mut() / chunk_mut() 交出去的那段。往里面写了数据、但还没有 set_len / advance_mut 就调 reserve,写进去的东西不保证还在原来的位置,也不保证还在:

分支未使用的容量里的数据
挪回开头(两种形态)原地没动,但 ptr 往前挪了,它相对 ptr 的位置变了
Vec::reserve / 唯一时扩大Vec 只保证 len 以内的字节;扩大前特意 set_len(offset + len) 就是这个原因
共享时另起新 Vec只拷贝 len 个字节,新块的未使用部分是未初始化内存

第一种最隐蔽,因为程序不会崩,只是读到另一段旧数据。下面的程序先读走 6 个字节,在未使用的容量开头写 "XYZ",再 reserve:

use bytes::{Buf, BufMut, BytesMut};

#[path = "../common.rs"]
mod common;
use common::*;

// 读出备用容量里 [from, to) 这几个字节。调用方保证它们以前写过(已初始化)
fn spare(buf: &mut BytesMut, from: usize, to: usize) -> String {
  let spare = buf.spare_capacity_mut();
  let bytes: Vec<u8> = spare[from..to].iter().map(|b| unsafe { b.assume_init() }).collect();
  String::from_utf8(bytes).unwrap()
}

fn main() {
  // 1. 往备用容量里写了东西但还没 set_len,然后 reserve
  let mut buf = BytesMut::with_capacity(16);
  let base = buf.as_ptr() as usize;
  buf.put_slice(b"abcdefgh");
  buf.advance(6); // 读走 6 个字节:KIND_VEC,off=6,剩 "gh"
  buf.spare_capacity_mut()[..3].iter_mut().zip(b"XYZ").for_each(|(d, s)| { d.write(*s); });
  let sp = spare(&mut buf, 0, 3);
  println!("before: {} data={:?} spare[0..3]={sp:?}", state(&buf, base, 16), buf);
  buf.reserve(12);
  let (sp, sp6) = (spare(&mut buf, 0, 3), spare(&mut buf, 6, 9));
  println!("after:  {} data={:?} spare[0..3]={sp:?} spare[6..9]={sp6:?}", state(&buf, base, 16), buf);

  // 2. extend_from_within:先 reserve(可能把数据挪走),再按新的 as_ptr 取源
  let mut buf = BytesMut::with_capacity(16);
  let base = buf.as_ptr() as usize;
  buf.put_slice(b"abcdefghijklmnop");
  buf.advance(12); // 剩 "mnop",off=12,cap=4
  println!("before: {} data={:?}", state(&buf, base, 16), buf);
  let s = snap();
  buf.extend_from_within(..);
  let d = delta(s);
  println!("after:  {} data={:?}  {d}", state(&buf, base, 16), buf);
}

前两行是第 1 部分(第 2 部分在本页最后一节用)。跑 3 次,输出相同:

before: ptr=base+6 len=2 cap=10 data=b"gh" spare[0..3]="XYZ"
after:  ptr=base+0 len=2 cap=16 data=b"gh" spare[0..3]="cde" spare[6..9]="XYZ"
before: ptr=base+12 len=4 cap=4 data=b"mnop"
after:  ptr=base+0 len=8 cap=16 data=b"mnopmnop"  alloc+0 realloc+0 (0B) free+0

reserve(12) 走了 KIND_VEC 的挪回开头:"gh" 搬到 base+0,未使用的容量从 base+2 开始,开头三个字节是早就读走的 "cde";"XYZ" 还在 base+8,变成了未使用容量的第 6 到 8 个字节。(程序只读以前写过的字节,所以 assume_init 是合法的。)如果紧接着 set_len(5) 把它当成刚读到的数据,得到的是 "ghcde"。

正确的顺序是先 reserve 再写,或者写完立刻 set_len / advance_mut,中间不调任何可能 reserve 的方法。hyper 的 poll_read_from_io 就是先 reserve(next),再取 chunk_mut() 交给 socket,读完马上 advance_mut(n)。

📝NOTE
冷知识:这句文档是因为别的库差点合入不健全的代码才加的

1.12.0 之前,reserve 的文档只说会尽量回收空间,没说未使用容量里的数据会怎样。PR #808(「Document that BytesMut::{reserve,try_reserve} doesn't preserve unused capacity」,标题里的 try_reserve 实际改的是 try_reclaim)的说明是:「This documents that BytesMut may not preserve the memory outside of 0..len after calling BytesMut::reserve. It may for example discard all of it, and replace it with uninit memory in the fallback case, or even shift it causing a different region of the BytesMut to appear when going through the reclaim optimization.」前一种是上表的「另起新 Vec」,后一种就是上面实验里的 "cde"。

起因写在同一段:「This was encountered in https://github.com/snapview/tungstenite-rs/pull/524#discussion_r2554375051 after #805 was closed, which could have lead to unsound code being merged into tungstenite.」issue #805(「std::io::Read into BytesMut efficiency」)问的是能不能不每次清零就把 BytesMut 的整段容量交给 Read。现在:reserve 和 try_reclaim 的文档里各有那一句,代码没有变。CHANGELOG 记在 1.12.0 的 Documented 一节。

1.11.1:一个没检查的加法

KIND_ARC 唯一分支里的 new_cap_plus_offset 是 1.11.1 才有的。修复提交是 d0293b0,标题是「Merge commit from fork」,正文列了两条子提交「Add repro for integer overflow」和「Always check overflow in new_cap + offset」,git tag --contains 给出的第一个版本是 v1.11.1,CHANGELOG 只有一句「Fix integer overflow in BytesMut::reserve」。git show d0293b0 的 src/bytes_mut.rs 部分:

                 let offset = self.ptr.as_ptr().offset_from(ptr) as usize;
 
+                let new_cap_plus_offset = match new_cap.checked_add(offset) {
+                    Some(new_cap_plus_offset) => new_cap_plus_offset,
+                    None if !allocate => return false,
+                    None => panic!("overflow"),
+                };
+
                 // Compare the condition in the `kind == KIND_VEC` case above
                 // for more details.
-                if v_capacity >= new_cap + offset {
+                if v_capacity >= new_cap_plus_offset {
                     self.cap = new_cap;
                     // no copy is necessary
                 } else if v_capacity >= new_cap && offset >= len {
@@ -713,14 +719,12 @@ impl BytesMut {
                     if !allocate {
                         return false;
                     }
-                    // calculate offset
-                    let off = (self.ptr.as_ptr() as usize) - (v.as_ptr() as usize);
 
                     // new_cap is calculated in terms of `BytesMut`, not the underlying
                     // `Vec`, so it does not take the offset into account.
                     //
                     // Thus we have to manually add it here.
-                    new_cap = new_cap.checked_add(off).expect("overflow");
+                    new_cap = new_cap_plus_offset;

旧代码里 new_cap + offset 在扩大分支里是检查过的(checked_add(off).expect("overflow")),但第一个 if 的比较里没检查。debug 构建下溢出直接 panic;release 构建默认不检查整数溢出,new_cap + offset 回绕成一个很小的数,v_capacity >= 小数 成立,于是执行 self.cap = new_cap:cap 被设成一个接近 usize::MAX、远超真实分配的值。之后 spare_capacity_mut、chunk_mut 都相信这个 cap,交出越界的切片。安全公告 GHSA-434x-w66g-qw3r(CVE-2026-25541、RUSTSEC-2026-0007,等级 high)的描述是:「When new_cap + offset overflows usize in release builds, this condition may incorrectly pass, causing self.cap to be set to a value that exceeds the actual allocated capacity.」同一个提交给 tests/test_bytes.rs 加了公告里的复现 bytes_mut_reserve_overflow,并在 ci/miri.sh 末尾加了 cargo miri test --release(miri 默认按 debug 跑,走不到回绕;见 源码地图)。

用公告里的复现,只做到 reserve 为止(不写入,避免真的越界),分别在 1.11.0 和 1.12.1 上跑。1.11.0 那个 crate 依赖 bytes = "=1.11.0":

use bytes::BytesMut;

// 同一个复现,只做到 reserve 为止:不写入,只看它报告的容量
fn main() {
  let r = std::panic::catch_unwind(|| {
    let mut a = BytesMut::from(&b"hello world"[..]);
    let mut b = a.split_off(5);
    drop(a);
    b.reserve(usize::MAX - 6);
    (b.len(), b.capacity())
  });
  match r {
    Ok((len, cap)) => println!("no panic, len={len} cap={cap}"),
    Err(e) => println!("panicked: {:?}", e.downcast_ref::<&str>()),
  }
  println!("debug_assertions: {}", cfg!(debug_assertions));
}

1.11.0,--release,跑 3 次,输出相同:

no panic, len=6 cap=18446744073709551615
debug_assertions: false

1.11.0,debug 构建,跑 3 次,除了线程号以外输出相同:


thread 'main' (1737978) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bytes-1.11.0/src/bytes_mut.rs:700:34:
attempt to add with overflow
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
panicked: Some("attempt to add with overflow")
debug_assertions: true

b 是 "hello world" 后 6 个字节,offset=5、len=6,整块只有 11 字节。release 下 reserve 正常返回,cap 却是 usize::MAX;这时再 put_u8 就写到了 11 字节分配之外。

1.12.1 上用公告原文的版本,带上那次越界的 put_u8:

use bytes::{BufMut, BytesMut};

// GHSA-434x-w66g-qw3r 里的复现,放进 catch_unwind 看 1.12.1 的行为
fn main() {
  let r = std::panic::catch_unwind(|| {
    let mut a = BytesMut::from(&b"hello world"[..]);
    let mut b = a.split_off(5);
    drop(a);
    b.reserve(usize::MAX - 6);
    b.put_u8(b'h');
    b.capacity()
  });
  match r {
    Ok(cap) => println!("no panic, cap={cap}"),
    Err(e) => println!("panicked: {:?}", e.downcast_ref::<&str>()),
  }
  println!("debug_assertions: {}", cfg!(debug_assertions));
}

--release 跑 3 次,除了线程号以外输出相同:


thread 'main' (1744703) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bytes-1.12.1/src/bytes_mut.rs:720:29:
overflow
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
panicked: Some("overflow")
debug_assertions: false

第 720 行就是新加的 None => panic!("overflow"),put_u8 根本走不到。debug 构建下 panic 信息和位置相同。现在无论 debug 还是 release,结果都是确定的 panic,和 len + additional 溢出时一样。

try_reclaim 与 allocate 参数

try_reclaim 是 1.7.0(PR #686,「Allow reclaiming the current allocation」)加的。PR 说明是「This is based on #680, where it was noted that it is hard to use BytesMut without additional allocations in some circumstances.」它和 reserve 共用 reserve_inner,allocate 这个参数就是那次加的:

    #[inline]
    #[must_use = "consider BytesMut::reserve if you need an infallible reservation"]
    pub fn try_reclaim(&mut self, additional: usize) -> bool {
        let len = self.len();
        let rem = self.capacity() - len;

        if additional <= rem {
            // The handle can already store at least `additional` more bytes, so
            // there is no further work needed to be done.
            return true;
        }

        self.reserve_inner(additional, false)
    }

allocate == false 时,reserve_inner 在每个需要分配的地方提前返回 false,此时 self 没有被改动:

位置reservetry_reclaim
KIND_VEC,不能挪回开头Vec::reservefalse
KIND_ARC 时 len + additional 或 new_cap + offset 溢出panic!("overflow")false
KIND_ARC 唯一,需要扩大reallocfalse
KIND_ARC 共享另起新 Vecfalse

不分配的三种结果(KIND_VEC 挪回开头、唯一时只改 cap、唯一时挪回开头)两者完全一样。开头那个程序的 try_reclaim 行正是这样:凡是 reserve 那行出现 alloc+1 或 realloc+1 的,try_reclaim 都是 false,前后状态一致;其余各行结果与 reserve 相同。#[must_use] 提醒调用方:返回 false 时容量没有变,不能接着往里写。

📝NOTE
没有蠢问题

问: try_reclaim 返回 true,是不是说明这次什么都没拷贝?

答: 不是。它保证的是不分配,不是不拷贝。挪回开头的两个分支都要用 copy_nonoverlapping 拷贝 len 个字节,try_reclaim 照样会走。摊还条件 offset >= len 限制的是拷贝的总量,不是有没有拷贝。想确认缓冲已经整块可用、而且不需要搬数据,可以在 len == 0 时调它,这时拷贝的是 0 个字节。

extend_from_within(1.12.0)

extend_from_within 是 1.12.0 新加的(PR #818,关闭 issue #817),把自己的一段复制到末尾,和 Vec::extend_from_within 对应。issue 里的理由是原来只能先 put_bytes 占位再 copy_within,「This touches memory twice and is inefficient in hot paths.」

    pub fn extend_from_within(&mut self, range: impl core::ops::RangeBounds<usize>) {
        let (begin, end) = crate::range(range, self.len());

        let cnt = end - begin;
        self.reserve(cnt);

        // SAFETY: range is already checked
        let src = unsafe { self.as_ptr().add(begin) };
        let dst = self.spare_capacity_mut();

        // SAFETY: range doesn't overlap with spare capacity
        unsafe { ptr::copy_nonoverlapping(src, dst.as_mut_ptr().cast(), cnt) }

        // SAFETY: capacity is already reserved and filled with data
        unsafe { self.advance_mut(cnt) }
    }

顺序是关键:先用 self.len() 检查范围,再 reserve(cnt),之后才取 self.as_ptr()。reserve 可能把数据挪回开头或者搬到新的分配里,reserve 之前拿到的指针可能已经指错了地方甚至悬空。源 [begin, end) 在 len 以内,目标在 len 之后的未使用容量里,两者不重叠,所以用 copy_nonoverlapping。

上面程序的第 2 部分(后两行输出):16 字节的缓冲读走 12 个,剩 "mnop",off=12、cap=4。extend_from_within(..) 里的 reserve(4) 走了 KIND_VEC 的挪回开头(0 + 12 ≥ 4,12 ≥ 4),ptr 从 base+12 回到 base+0,然后从新的 as_ptr() 拷贝,得到 "mnopmnop",没有分配。

动脑筋答案

跑 3 次,输出相同:

reserve(32) -> ptr=base+0 len=8 cap=40  alloc+0 realloc+0 (0B) free+0
reserve(56) -> ptr=base+0 len=8 cap=64  alloc+0 realloc+0 (0B) free+0
  • reserve(32):split_off(8) 让 buf 成了 KIND_ARC,后一半 drop 后计数为 1,走唯一分支。offset=0、new_cap = 8 + 32 = 40,整块容量 64 ≥ 40 + 0,第一种结果「只改 cap」:cap 被设成 new_cap,也就是 40,而不是整块剩下的 64。不拷贝、不分配。
  • reserve(56):剩余 40 − 8 = 32 < 56,又进 reserve_inner。new_cap = 64,64 ≥ 64 + 0,还是「只改 cap」,这次正好是整块的 64。
  • 所以唯一分支给的 cap 恰好是 len + additional,不会主动把整块交出来;要多少给多少,剩下的下次 reserve 再拿,也不用分配。capacity() 在这里小于底层分配的大小。

小结

  • reserve(n) 先看剩余容量,够了直接返回;不够才进 reserve_inner(n, allocate),按 KIND_VEC / KIND_ARC 和是否唯一分成几路。
  • KIND_VEC:收回前面读走的 off 之后放得下、并且 off >= len,就把数据挪回开头,不分配;否则拼回 Vec 交给 Vec::reserve,按 Vec 的规则至少翻倍,前面的 off 字节被带进新分配。off >= len 是 1.2.0(#555)为摊还复杂度加的。
  • KIND_ARC 且唯一:整块放得下就只把 cap 设成 len + additional;放不下但 offset >= len 就挪回开头,cap 变成整块容量;都不行就把整块 Vec 扩大到 max(2 × 整块容量, len + additional + offset)。
  • KIND_ARC 且共享:另分配一个 max(len + additional, 原始容量) 的 Vec,只拷 len 个字节,放掉旧的引用,回到 KIND_VEC;旧块等别的句柄 drop。
  • 原始容量只在 from_vec 时记录(不到 1 KiB 记 0,最多 64 KiB),只在共享分支起作用。with_capacity(0) 再 reserve 的缓冲(hyper 的读缓冲)原始容量是 0;hyper 每次 reserve 至少 8192,所以不受影响,每次只 reserve 一小段的代码才会多出 realloc。
  • reserve 不保留 [len, cap) 里的数据(1.12.0 文档,#808):先 reserve 再写,写完立刻 advance_mut。
  • 1.11.1 修了唯一分支里 new_cap + offset 未检查的加法:release 构建下它会回绕,把 cap 设成超出真实分配的值(GHSA-434x-w66g-qw3r)。现在溢出一律 panic!("overflow")。
  • try_reclaim(1.7.0,#686)走同一个函数、allocate = false,凡是要分配的地方返回 false;它保证不分配,不保证不拷贝。extend_from_within(1.12.0,#818)先 reserve,再按新的 as_ptr() 取源。

💬 关联讨论

发起讨论
加载中...