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
#[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。
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 设成整块容量 | 无 |
| 其他 | 扩大整块 Vec | realloc |
第二行的 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:
additional | new_cap + offset | 2 × 64 | 取 | realloc 大小 | 新 cap(减去 offset) |
|---|---|---|---|---|---|
| 60 | 90 + 10 = 100 | 128 | 128 | 128B | 118 |
| 100 | 130 + 10 = 140 | 128 | 140 | 140B | 130 |
两个细节。checked_shl(1) 只在移位位数不小于 usize 的位数时返回 None,这里移 1 位,它永远是 Some,起不到溢出检查的作用;真正防溢出的是前面两个 checked_add。注释里提到的 reserve_exact 在 BytesMut 上并不存在。
平台 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
}
- 新容量是
max(len + additional, 原始容量),用Vec::with_capacity精确分配,不翻倍。 - 只拷贝
len个有效字节。 - 拷完才
release_shared:先放手的话,别的句柄可能恰好在这时 drop,把整块释放掉,拷贝就读到了已释放的内存。 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、1023 | 0 |
| 1024 | 1024 |
| 5000 | 4096 |
| 8192 | 8192 |
| 65536、1 MiB | 65536 |
第一行是 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)。
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 没有被改动:
| 位置 | reserve | try_reclaim |
|---|---|---|
KIND_VEC,不能挪回开头 | Vec::reserve | false |
KIND_ARC 时 len + additional 或 new_cap + offset 溢出 | panic!("overflow") | false |
KIND_ARC 唯一,需要扩大 | realloc | false |
KIND_ARC 共享 | 另起新 Vec | false |
不分配的三种结果(KIND_VEC 挪回开头、唯一时只改 cap、唯一时挪回开头)两者完全一样。开头那个程序的 try_reclaim 行正是这样:凡是 reserve 那行出现 alloc+1 或 realloc+1 的,try_reclaim 都是 false,前后状态一致;其余各行结果与 reserve 相同。#[must_use] 提醒调用方:返回 false 时容量没有变,不能接着往里写。
问: 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()取源。