reserve 之后,备用容量里写的字节变了
主线位置:构建主线(
BytesMut)上「写入」和「reserve」两步之间。写进[len, cap)的字节在set_len/advance_mut之前还不算数据,reserve在原地回收、扩大和另起新块之间选路时只照顾len以内的字节。
flowchart TB
w["写进备用容量"] --> r["reserve(n)"]
r --> k{走哪条路}
k -->|挪回开头| m["数据前移"]
k -->|"Vec::reserve 或扩大"| g["只保证 len 以内"]
k -->|有别的句柄| n["新 Vec 只拷 len"]
m --> s1["set_len 读到旧字节"]
g --> s2["不保证还在"]
n --> s3["未初始化内存"]
reserve_inner 的每条分支、原始容量的编码、1.12.0 补的那句文档,在 reserve 里逐条跑过。这一页从使用者这边看:哪种写法会踩到,症状是什么,怎么改。最后两节是 reserve 另外两个常让人意外的行为。
本页实验在 M2 Max、rustc 1.99.0、--release 下运行,依赖:
bytes = "=1.12.1"
复现:先 reserve,再 set_len
一个读循环的常见写法:把 socket 里读到的字节直接写进 spare_capacity_mut(),然后「顺手」为下一次读留够位置,最后才 set_len。下面用 read_into_spare 模拟那次读:
use bytes::{Buf, BufMut, BytesMut};
// 模拟一次 read:把 src 写进备用容量的开头,返回写了几个字节(还没 set_len)
fn read_into_spare(buf: &mut BytesMut, src: &[u8]) -> usize {
let spare = buf.spare_capacity_mut();
let n = src.len().min(spare.len());
spare[..n].iter_mut().zip(src).for_each(|(d, s)| {
d.write(*s);
});
n
}
fn run(label: &str, set_len_first: bool) {
let mut buf = BytesMut::with_capacity(16);
buf.put_slice(b"abcdefgh");
buf.advance(6); // 前 6 个字节已经解析完,剩 "gh"
let n = read_into_spare(&mut buf, b"XYZ");
if set_len_first {
// 先把读到的字节算进 len,再 reserve
unsafe { buf.set_len(buf.len() + n) };
buf.reserve(12);
} else {
// 先 reserve(为下一次 read 留位置),再 set_len
buf.reserve(12);
// [2, 5) 这三个字节以前写过(是旧数据 "cde"),所以这里不会读到未初始化内存
unsafe { buf.set_len(buf.len() + n) };
}
println!("{label:<22} data={:?} cap={}", buf, buf.capacity());
}
fn main() {
run("reserve, then set_len", false);
run("set_len, then reserve", true);
}
跑 3 次,输出相同:
reserve, then set_len data=b"ghcde" cap=16
set_len, then reserve data=b"ghXYZ" cap=26
第一行就是这个坑:读到的是 XYZ,set_len 之后数据却是 ghcde。程序没有 panic,也没有报错,cde 是第一次 put_slice 写进去、早就被 advance(6) 读走的旧字节。
第二行只是把两句换了个顺序,XYZ 在 len 以内,reserve 保留了它。容量从 16 变成 26,是因为这时 len 是 5,挪回开头最多腾出 16 − 5 = 11 个字节,不够 12,换成了 Vec::reserve(条件见下一节)。
为什么
第一行的 reserve(12) 走的是 KIND_VEC 的「挪回开头」:前面空着 6 个字节(advance 留下的偏移),len 只有 2,reserve_inner 的两个条件都满足:挪回之后有 16 − 2 = 14 个字节可用,不少于 12;off >= self.len(),拷贝的量不超过前移的距离。于是:
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);
只拷了 self.len 个字节(gh),ptr 往前挪了 6。XYZ 原地没动,还在缓冲的第 8 到 10 字节,可是相对新的 ptr,备用容量的开头现在是第 2 到 4 字节,那里放着 cde。set_len(5) 认领的就是这三个字节。
三条路对备用容量各是什么结果,reserve 的「不保留未使用的容量」一节有一张表,这里只列结论:
reserve 走的路 | set_len 认领到的字节 |
|---|---|
| 挪回开头 | 缓冲里更早的旧数据(上面的 cde) |
Vec::reserve,或唯一时扩大 | Vec 只保证 len 以内,之外的字节不保证还在 |
还有别的句柄,另起新 Vec | 新块里只拷了 len 个字节,后面是未初始化内存,读它是未定义行为 |
reserve 的文档自 1.12.0 起写明了这一点:「This method does not preserve data stored in the unused capacity.」try_reclaim 的文档里也有同一句。
issue #562 的标题是「Bytes assumes that Vec::reserve retains data in the unused capacity」。KIND_ARC 唯一分支扩大缓冲时,会把 Shared 里的 Vec 拿出来调 Vec::reserve;而 BytesMut 的数据长度记在自己的 len 里,不在那个 Vec 的长度里。从 Vec 的角度看,BytesMut 写进去的数据可能正落在它的「未使用的容量」里,Vec::reserve 并不保证搬过去。
修复是 PR #563(「Don't have important data in unused capacity when calling reserve」,首次发布于 1.3.0),只加了 9 行:在 v.reserve 之前先 set_len(offset + len),并留下了这段注释,1.12.1 里还在 bytes_mut.rs 第 762 行:
// 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`.
CHANGELOG 1.3.0 的 Fixed 一节记为「Don't have important data in unused capacity when calling reserve (#563)」。bytes 对 Vec 的要求,和本页对 BytesMut 使用者的要求是同一条:想保住的字节,调 reserve 之前要先算进长度。
修法
- 写完立刻提交长度。 往
spare_capacity_mut()或chunk_mut()里写完,马上set_len/advance_mut,中间不调任何可能reserve的方法。 - 要预留就先预留。 需要更多空间时,先
reserve,再取spare_capacity_mut()/chunk_mut()写。hyper 的读缓冲就是这个顺序:先reserve(next),再把chunk_mut()交给 socket,读完马上advance_mut(n)(读写缓冲)。 - 注意隐式的
reserve。extend_from_slice第一行就是self.reserve(cnt),put_slice、put_u8这些BufMut方法也会走到它(BufMut 与 UninitSlice)。它们还会从len处开始写,直接覆盖备用容量的开头。备用容量里有没提交的数据时,这些方法都不能调。 - 不需要手动操作未初始化内存时,直接用
put_slice/extend_from_slice,长度由它们维护。
另外两个意外:reserve 的新容量
下面两条不会读错数据,表现是分配次数比预想的多。用同一个程序看:每轮 reserve(512)、写 512 字节、split().freeze() 切成一帧,1000 帧全部留着不 drop。公共的 common.rs 是一个包住 System 的计数分配器,count 返回闭包执行期间的 alloc、realloc、free 次数和新申请的字节数。
use bytes::{BufMut, Bytes, BytesMut};
#[path = "../common.rs"]
mod common;
use common::*;
// 每轮 reserve(512)、写 512 字节、整段切走冻结成一帧;所有帧都留着不 drop
fn run(label: &str, mut buf: BytesMut) {
let mut frames: Vec<Bytes> = Vec::with_capacity(1000);
let mut caps = Vec::new();
let (_, d) = count(|| {
for _ in 0..1000 {
buf.reserve(512);
if caps.len() < 4 {
caps.push(buf.capacity());
}
buf.put_bytes(b'x', 512);
frames.push(buf.split().freeze());
}
});
println!("{label:<34} first caps={caps:?} {d}");
}
fn main() {
run("with_capacity(8192)", BytesMut::with_capacity(8192));
let mut b = BytesMut::with_capacity(0);
b.reserve(8192);
run("with_capacity(0) + reserve(8192)", b);
run("new()", BytesMut::new());
}
跑 3 次,输出相同:
with_capacity(8192) first caps=[8192, 7680, 7168, 6656] alloc+126 realloc+0 (510456B) free+0
with_capacity(0) + reserve(8192) first caps=[8192, 7680, 7168, 6656] alloc+1970 realloc+0 (543240B) free+0
new() first caps=[512, 512, 512, 512] alloc+2001 realloc+0 (552032B) free+0
三种缓冲写的是同样的 1000 帧,分配次数差了 15 倍。
有切片活着时,不翻倍
每次 split() 之后,buf 只剩后面没写的容量;容量用完、再 reserve 时,前面的帧都还活着,走的是 KIND_ARC 共享分支。这条路只能另起一个 Vec,容量是 max(len + additional, 原始容量),用 Vec::with_capacity 精确分配,没有 Vec 那样的翻倍。
with_capacity(8192):原始容量 8192 胜出,每块新缓冲 8192 字节,能装 16 帧。新缓冲 62 块,每块第一次split时再分配一个 40 字节的Shared(split 与 freeze),共 63 个(包括最初那块),加上caps那一次,126 次。new():原始容量是 0,每轮的新缓冲正好是0 + 512字节,写一帧就满。每帧一块缓冲加一个Shared,2000 次,加caps一次。
Vec<u8> 用惯了容易以为 reserve 会「多给一些」。BytesMut 只有在独占整块分配时才会翻倍(KIND_VEC 交给 Vec::reserve,或 KIND_ARC 唯一时扩大);只要还有别的句柄,给的就是需求和原始容量中较大的那个。
with_capacity(0) 出生的缓冲没有原始容量
第二行最容易误会:它和第一行一样,开局 reserve(8192) 之后容量就是 8192,前 16 帧的容量序列也一模一样。区别出在第一块用完之后:它每轮分配 512 字节,和 new() 一样,1970 次。
原始容量只在 from_vec 里根据创建时的容量记一次。with_capacity(0) 记下的是 0,之后 Vec::reserve 扩到 8192 不会更新它,共享分支另起新块时也把旧值原样带过去。所以一个以容量 0 出生的缓冲,原始容量永远是 0;不到 1 KiB 的容量也记成 0。编码和对照表见 data 字段的编码。
hyper 的读缓冲正是 with_capacity(0) 出生的,但它每次 reserve 的量本身不小于 8192,原始容量不起作用,reserve 的「实测:一个类 hyper 的读循环」量过。吃亏的是每次只 reserve 一小段、又让切片活得比较久的代码。
修法:
- 需要原始容量起作用,就用
BytesMut::with_capacity(n)创建,n不小于 1024,而不是先new()再reserve(n)。 - 或者每次
reserve的量本身就够大(像 hyper 那样)。 - 帧切出来就拷走或尽快 drop,让
reserve有机会走唯一分支,原地收回整块缓冲。
怎么确认自己没踩
- 在代码里搜
spare_capacity_mut、chunk_mut、set_len、advance_mut:每一处「写」和「提交长度」之间,不能有reserve、try_reclaim、extend_from_slice、put_*、unsplit之类可能reserve的调用。 - 读循环的分配次数可以用计数分配器量:每轮都在分配,又有帧被留着,看创建方式是不是
new()/with_capacity(0),以及每次reserve的量。
小结
reserve只保留len以内的字节。写进备用容量还没提交的数据,可能被换成旧字节(挪回开头)、不保证还在(Vec::reserve),或者变成未初始化内存(另起新块)。- 挪回开头这条最隐蔽:程序不崩,
set_len认领到的是早就读走的数据。 - 修法是写完立刻
set_len/advance_mut,或者先reserve再写;extend_from_slice和put_*内部都会reserve。 - 还有切片活着时,
reserve按max(len + additional, 原始容量)精确分配,不翻倍。 - 原始容量只在创建时记一次,
with_capacity(0)或new()出生的缓冲永远是 0;想让它起作用,就用不小于 1 KiB 的with_capacity创建。