Rust in Everything
基于 bytes 1.12.1

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 的文档里也有同一句。

📝NOTE
冷知识:bytes 自己也踩过这个坑

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 创建。

💬 关联讨论

发起讨论
加载中...