Rust in Everything
基于 bytes 1.12.1

BufMut 与 UninitSlice

主线位置:构建主线的第 2 步「写入」。BytesMut::with_capacity 给出一段还没初始化的容量,BufMut 规定怎么往里写:chunk_mut 交出空位,调用方写好,advance_mut 把这几个字节算进 len。写满时 chunk_mut 先 reserve(64),那一步走reserve;写完之后的 split_to、freeze 在 split 与 freeze。

flowchart TB
  ps["默认的 put_slice"] --> cm["chunk_mut"]
  cm -->|容量已满| rs["reserve(64)"]
  cm -->|还有空位| us["UninitSlice:只许写"]
  rs --> us
  us --> wr["copy_from_slice 等写入"]
  wr --> am["advance_mut:unsafe"]
  am --> ln["len 加上 n"]

BufMut 是 Buf 的写方向:Buf 有一个读游标,BufMut 有一个写游标。buf_mut.rs 1 671 行里大部分是 put_u16、put_i64_le 这类方法的文档和示例,机制集中在三个必需方法、put / put_slice 两个默认实现,以及 Vec<u8>、&mut [u8]、&mut [MaybeUninit<u8>] 三个实现上;BytesMut 的实现在 bytes_mut.rs。chunk_mut 返回的 UninitSlice 在 buf/uninit_slice.rs,257 行。

三个必需方法,一个 unsafe trait

去掉文档,trait 的必需部分是:

pub unsafe trait BufMut {
    fn remaining_mut(&self) -> usize;

    unsafe fn advance_mut(&mut self, cnt: usize);

    // ...
    fn chunk_mut(&mut self) -> &mut UninitSlice;
    // ...
}
方法含义文档里给实现者的约定
remaining_mut从当前位置到缓冲末尾还能写多少不小于 chunk_mut() 的长度;只有 advance_mut 等改变位置的调用才能让它变;可以比真实空间小
chunk_mut从当前位置开始的一段可写空间,长度在 0 到 remaining_mut() 之间不能 panic;返回空切片当且仅当 remaining_mut() 为 0;需要时可以分配
advance_mut写游标前进 cnt调用方保证接下来 cnt 个字节已经初始化;cnt > remaining_mut() 时建议 panic;cnt == 0 不能 panic

chunk_mut 和 Buf::chunk 一样只给一段:底层可以不连续(比如两个缓冲用 chain_mut 接起来),写满这一段、advance_mut 之后再要下一段。

trait 本身是 unsafe trait,实现它要写 unsafe impl。原因写在 PR #432(0.6.0)里:「Users of BufMut are unable to defend against incorrect implementations of BufMut, this makes the trait unsafe to implement.」它修的 issue #329 列出了 unsafe 代码会依赖的性质:remaining_mut 返回正确的值、chunk_mut 除了 advance 之外总返回同一段。下面的 put_slice 默认实现就是这样的代码:它拿 chunk_mut() 的长度当作「可以写这么多、写完可以 advance_mut 这么多」,实现者如果返回了比真实空间长的切片,越界写就发生在 bytes 自己的 unsafe 块里。

chunk_mut 为什么不返回 &mut [u8]

BytesMut::with_capacity(16) 之后,那 16 个字节是分配器给的原样内存,从没被写过。Rust 规定读未初始化的内存是未定义行为,u8 也不例外,所以这段内存不能做成 &mut [u8] 交出去:safe 代码拿到 &mut [u8] 就可以读它。标准库表示「可能没初始化」的类型是 MaybeUninit<u8>,UninitSlice 就是包了一层的 [MaybeUninit<u8>]:

/// Uninitialized byte slice.
///
/// Returned by `BufMut::chunk_mut()`, the referenced byte slice may be
/// uninitialized. The wrapper provides safe access without introducing
/// undefined behavior.
///
/// The safety invariants of this wrapper are:
///
///  1. Reading from an `UninitSlice` is undefined behavior.
///  2. Writing uninitialized bytes to an `UninitSlice` is undefined behavior.
///
/// The difference between `&mut UninitSlice` and `&mut [MaybeUninit<u8>]` is
/// that it is possible in safe code to write uninitialized bytes to an
/// `&mut [MaybeUninit<u8>]`, which this type prohibits.
#[repr(transparent)]
pub struct UninitSlice([MaybeUninit<u8>]);

两条不变式各防一件事。第一条防「读到没初始化的字节」。第二条看起来多余,原因在于 chunk_mut 交出的内存不一定是未初始化的:&mut [u8] 的 BufMut 实现交出的就是一段已经初始化的数组。如果交出去的是 &mut [MaybeUninit<u8>],safe 代码可以往里写 MaybeUninit::uninit(),原数组的主人之后照常当 [u8] 读,就读到了未初始化的字节。#[repr(transparent)] 保证它和 [MaybeUninit<u8>] 布局相同,所以几个构造函数都只是一次指针转换。

📝NOTE
冷知识:chunk_mut 的返回类型改过两次

0.4 时这个方法叫 bytes_mut,签名是 unsafe fn bytes_mut(&mut self) -> &mut [u8]:把未初始化内存当 [u8] 交出去,靠 unsafe fn 提醒调用方别读。PR #305(「Change BufMut methods that expose maybe-uninitialized bytes」,0.5.0)把它改成安全的 fn bytes_mut(&mut self) -> &mut [MaybeUninit<u8>],理由是「BufMut::bytesMut and BufMut::bytes_vectored_mut are no longer unsafe fn, since the types encapsulate the unsafety instead」。

问题出在 &mut [u8] 的实现上。issue #328 的标题是「Implementation of BufMut for &mut [u8] is unsound」,复现只有两行 safe 代码:BufMut::bytes_mut(&mut buf)[0] = MaybeUninit::uninit();,然后读 buf[0],「Reading this value is UB」。它指出把 &mut [T] 转成 &mut [MaybeUninit<T>] 本身就不安全,bytes_mut 必须返回一个「prevents overwriting value with invalid values」的新类型。PR #433(0.6.0)照做了:「The way BufMut uses MaybeUninit can lead to unsoundness. This replaces MaybeUnit with a type owned by bytes so we can ensure the usage patterns are sound.」新加的就是 UninitSlice,上面那条「Writing uninitialized bytes to an UninitSlice is undefined behavior」的不变式就是为这个 issue 写的。1.0.0 又把 bytes_mut 改名为 chunk_mut(#450),源码里留着 doc(alias = "bytes_mut") 方便旧名字搜到它。

UninitSlice 的全部 API

方法安全作用
new,参数 &mut [u8]是包一段已初始化的内存(&mut [u8] 的 chunk_mut 用它)
uninit,参数 &mut [MaybeUninit<u8>]是包一段未初始化的内存(BytesMut 的 chunk_mut 经 From 用它)
from_raw_parts_mut(ptr, len)否从裸指针构造(Vec<u8> 的 chunk_mut 用它)
write_byte(index, byte)是写一个字节,index 越界 panic
copy_from_slice(src)是assert_eq! 长度相等,再 copy_nonoverlapping
as_mut_ptr()是交出 *mut u8,给只认裸指针的写入者(read(2)、ptr::write_bytes)
as_uninit_slice_mut()否交出 &mut [MaybeUninit<u8>],调用方承诺不读、不写未初始化的值
len()是长度
下标是impl_index! 只给六种区间(..、a..b、a..、..b、a..=b、..=b),结果还是 UninitSlice

所有安全方法要么只写,要么只给指针。没有 Index<usize>:slice[0] 会产出一个 u8 值,也就是一次读。照着读的写法编译不过:

use bytes::{BufMut, BytesMut};

fn main() {
  let mut buf = BytesMut::with_capacity(16);
  let first = buf.chunk_mut()[0];
  println!("{first}");
}
error[E0277]: the type `UninitSlice` cannot be indexed by `{integer}`
 --> src/bin/read_uninit.rs:5:31
  |
5 |   let first = buf.chunk_mut()[0];
  |                               ^ `UninitSlice` cannot be indexed by `{integer}`
  |
  = help: the trait `Index<{integer}>` is not implemented for `UninitSlice`
  = help: `UninitSlice` implements trait `Index<Idx>`:
            Index<RangeFull>
            Index<RangeTo<usize>>
            Index<std::ops::Range<usize>>
            Index<std::ops::RangeFrom<usize>>
            Index<std::ops::RangeInclusive<usize>>
            Index<std::ops::RangeToInclusive<usize>>

For more information about this error, try `rustc --explain E0277`.
error: could not compile `exp-bytes-bufmut` (bin "read_uninit") due to 1 previous error

as_uninit_slice_mut 是唯一一个把内部 [MaybeUninit<u8>] 交出去的方法,所以是 unsafe fn。它存在是为了接上 tokio 的 ReadBuf::uninit,后者要的正是 &mut [MaybeUninit<u8>]。hyper 的 poll_read_from_io 在调用处写明了为什么满足约定:

        // SAFETY: ReadBuf and poll_read promise not to set any uninitialized
        // bytes onto `dst`.
        let dst = unsafe { self.read_buf.chunk_mut().as_uninit_slice_mut() };
        let mut buf = ReadBuf::uninit(dst);

tokio 自己的 read_buf 也是同样三步:chunk_mut、as_uninit_slice_mut、ReadBuf::uninit。ReadBuf 怎么保证「不把未初始化的值写回去」见 hyper 卷的 IO trait,hyper 每次读多少见读写缓冲。

📝NOTE
没有蠢问题

问: as_mut_ptr 是安全函数,拿到 *mut u8 之后照样能读未初始化的字节,这不是把第一条不变式绕过去了吗?

答: 拿裸指针是安全的,解引用裸指针不是。用 as_mut_ptr 读或写都得写 unsafe { ptr.read() } / unsafe { ptr.write(b) },责任已经转到调用方身上。UninitSlice 保证的是「只用 safe 代码不会出 UB」,它不负责 unsafe 块里的事。as_uninit_slice_mut 要标 unsafe 而 as_mut_ptr 不用,区别也在这里:前者交出的是一个能在 safe 代码里直接写 MaybeUninit::uninit() 的引用。

advance_mut 为什么是 unsafe

chunk_mut 只是交出空位,len 不变;写完要 advance_mut(n) 告诉缓冲「前 n 个字节现在是数据了」。Vec<u8> 的 advance_mut:

    #[inline]
    unsafe fn advance_mut(&mut self, cnt: usize) {
        let len = self.len();
        let remaining = self.capacity() - len;

        if remaining < cnt {
            panic_advance(&TryGetError {
                requested: cnt,
                available: remaining,
            });
        }

        // Addition will not overflow since the sum is at most the capacity.
        self.set_len(len + cnt);
    }

BytesMut 的版本是同一个形状,最后一行换成 self.len = self.len() + cnt。越界(超过容量)可以检查,所以两个实现都检查并 panic,信息是 advance out of bounds: the len is {} but advancing by {}。检查不了的是「这 cnt 个字节是不是真的写过」:BufMut 不知道调用方往 chunk_mut 里写了多少。set_len 之后,这些字节就能通过 Deref<Target = [u8]> 被 safe 代码读到。所以这条约定只能交给调用方,函数标 unsafe,文档的 Safety 一节只有一句:「The caller must ensure that the next cnt bytes of chunk are initialized.」

一个合规的写法:只用 UninitSlice 的安全方法写,或者把 as_mut_ptr 交给一个保证写满的函数,advance_mut 的数字与实际写入的一致。依赖:

bytes = "=1.12.1"
use std::mem::MaybeUninit;

use bytes::{BufMut, BytesMut};

fn main() {
  let mut buf = BytesMut::with_capacity(16);
  let dst = buf.chunk_mut();
  println!("chunk_mut: {:?}, len {}", dst, dst.len());

  // 只用 UninitSlice 的安全方法写:copy_from_slice 要求长度正好相等
  dst[..5].copy_from_slice(b"hello");
  dst.write_byte(5, b' ');
  // SAFETY: 上面刚初始化了 chunk_mut 的前 6 个字节
  unsafe { buf.advance_mut(6) };
  println!("after advance_mut(6): {:?}, len {}, cap {}", buf, buf.len(), buf.capacity());

  // 把未初始化的尾部交给一个「只认裸指针」的写入者,比如 read(2)
  let dst = buf.chunk_mut();
  let n = fake_read(dst.as_mut_ptr(), dst.len());
  // SAFETY: fake_read 保证写满了前 n 个字节
  unsafe { buf.advance_mut(n) };
  println!("after fake_read: {:?}", buf);

  // &mut [MaybeUninit<u8>] 也是 BufMut:栈上的未初始化数组,不用先清零
  let mut storage = [MaybeUninit::<u8>::uninit(); 8];
  let mut out = &mut storage[..];
  out.put_u32(0xdead_beef);
  out.put_slice(b"ok");
  println!("MaybeUninit slice: remaining_mut {}", out.remaining_mut());
  // SAFETY: 前 6 个字节刚由 put_u32 和 put_slice 写过
  let written = unsafe { std::slice::from_raw_parts(storage.as_ptr().cast::<u8>(), 6) };
  println!("written = {written:x?}");
}

// 模拟一个系统调用:往 ptr 写 len 以内的字节,返回写了多少
fn fake_read(ptr: *mut u8, len: usize) -> usize {
  let src = b"world";
  let n = src.len().min(len);
  // SAFETY: 调用方保证 ptr 对 len 个字节可写
  unsafe { std::ptr::copy_nonoverlapping(src.as_ptr(), ptr, n) };
  n
}

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

chunk_mut: UninitSlice[...], len 16
after advance_mut(6): b"hello ", len 6, cap 16
after fake_read: b"hello world"
MaybeUninit slice: remaining_mut 2
written = [de, ad, be, ef, 6f, 6b]

UninitSlice 的 Debug 只打印 UninitSlice[...],连格式化都不读内容。chunk_mut 的长度是 16,即全部剩余容量:BytesMut 的 chunk_mut 就是 spare_capacity_mut()。

remaining_mut:会增长的缓冲返回 isize::MAX - len

remaining_mut 对固定大小的缓冲就是剩余长度,&mut [u8] 返回 self.len()(这个切片本身就是「剩下的部分」,见后面)。对会自己扩容的 Vec<u8> 和 BytesMut,它返回的是「最多还能长多少」:

unsafe impl BufMut for Vec<u8> {
    #[inline]
    fn remaining_mut(&self) -> usize {
        // A vector can never have more than isize::MAX bytes
        isize::MAX as usize - self.len()
    }

BytesMut 的 remaining_mut 一样,注释是「Max allocation size is isize::MAX.」Rust 里单个分配不能超过 isize::MAX 字节,Vec 的容量上限就是它。两个实现是分两次改到这里的:Vec<u8> 在 1.1.0 由 PR #501(「Clarify BufMut allocation guarantees」)从 usize::MAX - self.len() 改过来;BytesMut 一直是 usize::MAX - self.len(),直到 1.11.0 的 PR #795(「Tweak BytesMut::remaining_mut」),说明只有一句:「Same as in Vec: max allocation size in Rust is isize::MAX.」

所以对 Vec 和 BytesMut 来说,remaining_mut 不是「不分配还能写多少」,那个数是 capacity() - len()。同一个 #501 还给 remaining_mut 的文档加了一句:「Implementations may fail before reaching the number of bytes indicated by this method if they encounter an allocation failure.」默认的 put、put_slice 用它做的唯一一件事是开头的容量检查,对会增长的缓冲这个检查实际上总能通过。

put 与 put_slice:默认实现

put_slice 的默认实现是一个「要一段、写一段、前进一段」的循环:

    #[inline]
    fn put_slice(&mut self, mut src: &[u8]) {
        if self.remaining_mut() < src.len() {
            panic_advance(&TryGetError {
                requested: src.len(),
                available: self.remaining_mut(),
            });
        }

        while !src.is_empty() {
            let dst = self.chunk_mut();
            let cnt = usize::min(src.len(), dst.len());

            dst[..cnt].copy_from_slice(&src[..cnt]);
            src = &src[cnt..];

            // SAFETY: We just initialized `cnt` bytes in `self`.
            unsafe { self.advance_mut(cnt) };
        }
    }

dst[..cnt] 是 UninitSlice 的区间下标,copy_from_slice 要求两边长度相等,所以两边都切成 cnt。每次只写 chunk_mut 给的那一段,所以对不连续的 BufMut 也成立。put_u8 是 self.put_slice(&[n]),put_u16 到 put_i128_ne 都是 self.put_slice(&n.to_be_bytes()) 一类,put_f32 / put_f64 先 to_bits() 再转给整数版本。实现者只要覆盖 put_slice,所有 put_* 都跟着变快。

put 把一个 Buf 整个写进来,循环里两边都可能分段,每次取 src.chunk() 和 self.chunk_mut() 中较短的一段。Buf::chunk 只给当前一段这件事,在 Buf 一页讲。

Vec<u8> 和 BytesMut 都覆盖了 put、put_slice、put_bytes,注释一样:「Specialize these methods so they can skip checking remaining_mut and advance_mut.」put_slice 直接是 extend_from_slice。Vec<u8> 的 put 先 reserve(src.remaining()) 再逐段 extend_from_slice。

BytesMut::put:容量为 0 时接管源的分配

BytesMut 的 put 多了一条路:

    fn put<T: Buf>(&mut self, mut src: T)
    where
        Self: Sized,
    {
        if !src.has_remaining() {
            // prevent calling `copy_to_bytes`->`put`->`copy_to_bytes` infintely when src is empty
            return;
        } else if self.capacity() == 0 {
            // When capacity is zero, try reusing allocation of `src`.
            let src_copy = src.copy_to_bytes(src.remaining());
            drop(src);
            match src_copy.try_into_mut() {
                Ok(bytes_mut) => *self = bytes_mut,
                Err(bytes) => self.extend_from_slice(&bytes),
            }
        } else {
            // In case the src isn't contiguous, reserve upfront.
            self.reserve(src.remaining());

            while src.has_remaining() {
                let s = src.chunk();
                let l = s.len();
                self.extend_from_slice(s);
                src.advance(l);
            }
        }
    }

中间那条分支:自己一个字节都还没有(capacity() == 0,BytesMut::new() 就是这样)时,先让源把全部内容交成一个 Bytes,再用 try_into_mut 试着把它变回唯一拥有的 BytesMut。成功就把 *self 整个换掉,一个字节都不拷。能不能成功取决于两件事:源的 copy_to_bytes 是不是零拷贝(Bytes 的是 split_to,见切片),以及那块存储是不是只剩这一个句柄。drop(src) 放在 try_into_mut 之前,就是为了让源自己那个句柄先放掉引用。

这条分支不是按类型特化:任何 Buf 都会走进来,只是对 Bytes 这种 copy_to_bytes 不拷贝的源才省事。其他源的默认 copy_to_bytes 会 BytesMut::with_capacity(len) 再拷一遍,然后 try_into_mut 拿回刚分配的那块,总共还是一次分配、一次拷贝,和 put_slice 一样。

else 分支先 reserve(src.remaining()):源是 Chain 这类不连续的 Buf 时,一次预留够,不用每段 extend_from_slice 时各扩一次。

下面用计数分配器(同时数 alloc 和 realloc)把几种情况跑一遍。源都是 4096 字节,Bytes from Vec 是 Bytes::from(vec![7u8; 4096])(len == cap,可提升存储)。report 只数闭包里的分配,格式化在计数之后:

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

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

struct Counting;

static ALLOCS: AtomicUsize = AtomicUsize::new(0);
static REALLOCS: AtomicUsize = AtomicUsize::new(0);

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

#[global_allocator]
static GLOBAL: Counting = Counting;

fn counts() -> (usize, usize) {
  (ALLOCS.load(Ordering::Relaxed), REALLOCS.load(Ordering::Relaxed))
}

// 闭包返回 (说明, 数值),在计数之后才格式化,println 的分配不算进去
fn report(name: &str, f: impl FnOnce() -> (&'static str, usize)) {
  let (a0, r0) = counts();
  let (label, value) = f();
  let (a1, r1) = counts();
  println!("{name:<42} alloc +{} realloc +{}  {label} {value}", a1 - a0, r1 - r0);
}

fn main() {
  println!("start");
  let data = vec![7u8; 4096];

  // 源:Vec 转来的 Bytes(len == cap,可提升存储),唯一拥有
  let src = Bytes::from(data.clone());
  let p = src.as_ptr();
  report("BytesMut::new().put(Bytes from Vec)", || {
    let mut dst = BytesMut::new();
    dst.put(src);
    ("same buffer:", (dst.as_ptr() == p) as usize)
  });

  let src = Bytes::from(data.clone());
  report("BytesMut::new().put_slice(&bytes)", || {
    let mut dst = BytesMut::new();
    dst.put_slice(&src);
    ("len", dst.len())
  });
  drop(src);

  let src = Bytes::from(data.clone());
  let keep = src.clone();
  report("BytesMut::new().put(Bytes, 有别的 clone)", || {
    let mut dst = BytesMut::new();
    dst.put(src);
    ("same buffer:", (dst.as_ptr() == keep.as_ptr()) as usize)
  });
  drop(keep);

  let src = Bytes::from_static(&[7u8; 4096]);
  report("BytesMut::new().put(Bytes::from_static)", || {
    let mut dst = BytesMut::new();
    dst.put(src);
    ("len", dst.len())
  });

  let src = Bytes::from(data.clone());
  let p = src.as_ptr();
  report("BytesMut::with_capacity(1).put(Bytes)", || {
    let mut dst = BytesMut::with_capacity(1);
    dst.put(src);
    ("same buffer:", (dst.as_ptr() == p) as usize)
  });

  let src = Bytes::from(data.clone());
  report("Vec::new().put(Bytes)", || {
    let mut dst: Vec<u8> = Vec::new();
    dst.put(src);
    ("cap", dst.capacity())
  });

  report("BytesMut::new().put(&[u8])", || {
    let mut dst = BytesMut::new();
    dst.put(&data[..]);
    ("len", dst.len())
  });

  // #794:不连续的源,put 先 reserve 总长度
  let (a, b, c) = (vec![1u8; 1000], vec![2u8; 1000], vec![3u8; 1000]);
  report("with_capacity(1).put(a.chain(b).chain(c))", || {
    let mut dst = BytesMut::with_capacity(1);
    dst.put((&a[..]).chain(&b[..]).chain(&c[..]));
    ("cap", dst.capacity())
  });
  report("with_capacity(1) + 3 x put_slice", || {
    let mut dst = BytesMut::with_capacity(1);
    dst.put_slice(&a);
    dst.put_slice(&b);
    dst.put_slice(&c);
    ("cap", dst.capacity())
  });

  // #803:空源不再递归
  report("BytesMut::new().put(&[] as &[u8])", || {
    let mut dst = BytesMut::new();
    dst.put(&[] as &[u8]);
    ("cap", dst.capacity())
  });
}

跑 3 次,输出相同:

start
BytesMut::new().put(Bytes from Vec)        alloc +0 realloc +0  same buffer: 1
BytesMut::new().put_slice(&bytes)          alloc +1 realloc +0  len 4096
BytesMut::new().put(Bytes, 有别的 clone)      alloc +1 realloc +0  same buffer: 0
BytesMut::new().put(Bytes::from_static)    alloc +1 realloc +0  len 4096
BytesMut::with_capacity(1).put(Bytes)      alloc +1 realloc +1  same buffer: 0
Vec::new().put(Bytes)                      alloc +1 realloc +0  cap 4096
BytesMut::new().put(&[u8])                 alloc +1 realloc +0  len 4096
with_capacity(1).put(a.chain(b).chain(c))  alloc +1 realloc +1  cap 3000
with_capacity(1) + 3 x put_slice           alloc +1 realloc +3  cap 4000
BytesMut::new().put(&[] as &[u8])          alloc +0 realloc +0  cap 0

(第三行的中文标签占两列宽,对齐歪了,输出原样保留。)

情况结果原因
空 BytesMut 收一个唯一的 Bytes0 次分配,same buffer: 1split_to(len) 交出整段,try_into_mut 成功,dst 直接用源的缓冲
同样的数据用 put_slice1 次分配extend_from_slice,分配 4096 字节再拷贝
源还有别的 clone1 次分配,不同缓冲try_into_mut 返回 Err,退回 extend_from_slice
源是 from_static1 次分配静态存储的 try_into_mut 总是 Err
目标已有 1 字节容量with_capacity 1 次 + reserve 1 次 realloc容量不是 0,走 else 分支照常拷贝
Vec<u8> 收 Bytes1 次分配Vec 的 put 没有这条复用分支
空 BytesMut 收 &[u8]1 次分配默认 copy_to_bytes 分配并拷贝一次,try_into_mut 拿回同一块
三段 Chain1 次 realloc,容量 3000put 先 reserve(3000)
同样三段分三次 put_slice3 次 realloc,容量 4000每次 reserve(1000),按 Vec 的倍增规则扩

这三处改动都在 1.11.0:「Specialize BytesMut::put::<Bytes>」(PR #793)加了 capacity() == 0 的分支;「Reserve capacity in BytesMut::put」(PR #794)加了 else 里的 reserve,说明是「Copy-paste code from Vec::put」;最上面那条 has_remaining 检查来自 PR #803。

📝NOTE
冷知识:#793 的复用分支在空源上会无限递归

#793 刚合进去时,put 开头就是 if self.capacity() == 0。issue #802(「Impl of Buf with default copy_to_bytes impl could stack overflow」)指出:一个没有覆盖 copy_to_bytes 的 Buf,内容为空时,BytesMut::with_capacity(0) 的 put 走进复用分支,调用源的 copy_to_bytes(0);默认的 copy_to_bytes 是 BytesMut::with_capacity(len) 加 ret.put(self.take(len)),len 是 0,于是又是一个容量为 0 的 BytesMut 在 put,「and so on, and so on」。PR #803(「fix: BytesMut only reuse if src has remaining」)给出的最小复现更短:

let mut buf = BytesMut::new();
buf.put(&[] as &[u8]);

修法是在最前面加 if !src.has_remaining() { return; },注释就是上面代码里那句「prevent calling copy_to_bytes->put->copy_to_bytes infintely when src is empty」。#793 和 #803 都在 1.11.0 里发布,所以没有哪个正式版本带着这个递归。上面实验的最后一行就是这个复现,现在 0 次分配直接返回。

三个现成实现

buf_mut.rs 里有五个 unsafe impl:&mut T 和 Box<T> 只是用 deref_forward_bufmut! 把三个必需方法、put_slice 和 put_u8 到 put_i64_ne 转发给内层;真正有内容的是下面三个,加上 bytes_mut.rs 里的 BytesMut。

类型remaining_mutchunk_mutadvance_mut覆盖的 put_*
Vec<u8>isize::MAX - len满了先 reserve(64),交出 len..cap 这一段set_len,超过容量 panicput、put_slice(extend_from_slice)、put_bytes(resize)
BytesMutisize::MAX - len满了先 reserve(64),交出 spare_capacity_mut()len += cnt,超过容量 panicput、put_slice、put_bytes
&mut [u8](源码)切片长度UninitSlice::new(self),整个切片把切片前 cnt 个字节切掉put_slice、put_bytes
&mut [MaybeUninit<u8>](源码)切片长度UninitSlice::uninit(self)同上put_slice、put_bytes

Vec<u8>:chunk_mut 一次至少扩 64

Vec<u8> 的 chunk_mut:

    #[inline]
    fn chunk_mut(&mut self) -> &mut UninitSlice {
        if self.capacity() == self.len() {
            self.reserve(64); // Grow the vec
        }

        let cap = self.capacity();
        let len = self.len();

        let ptr = self.as_mut_ptr();
        // SAFETY: Since `ptr` is valid for `cap` bytes, `ptr.add(len)` must be
        // valid for `cap - len` bytes. The subtraction will not underflow since
        // `len <= cap`.
        unsafe { UninitSlice::from_raw_parts_mut(ptr.add(len), cap - len) }
    }

chunk_mut 的约定是「返回空切片当且仅当 remaining_mut() 为 0」,而 Vec 的 remaining_mut 几乎从不为 0,所以满了就必须先扩容,不能交出一个空切片。扩多少是 Vec::reserve(64) 决定的:标准库的 reserve 在需要扩时取 max(2 × cap, len + 64)。BytesMut 的 chunk_mut 是同样的写法:

    #[inline]
    fn chunk_mut(&mut self) -> &mut UninitSlice {
        if self.capacity() == self.len() {
            self.reserve(64);
        }
        self.spare_capacity_mut().into()
    }

BytesMut::reserve(64) 在 KIND_VEC、前面又没有可回收的空间时,同样落到 Vec::reserve,细节在 reserve。

❗IMPORTANT
动脑筋:从空缓冲开始写 300 个字节,容量依次是多少?

平台 M2 Max、rustc 1.99.0,--release。四种写法:Vec<u8> 和 BytesMut 各用 put_u8 写 300 次,再各用 chunk_mut().write_byte(0, ..) 加 advance_mut(1) 写 300 次,每次容量变化记一笔:

use bytes::{BufMut, BytesMut};

fn main() {
  // put_u8 -> put_slice -> extend_from_slice(Vec 与 BytesMut 都覆盖了 put_slice)
  let mut v: Vec<u8> = Vec::new();
  let mut caps = vec![v.capacity()];
  for i in 0..300 {
    v.put_u8(i as u8);
    if *caps.last().unwrap() != v.capacity() {
      caps.push(v.capacity());
    }
  }
  println!("Vec<u8>   put_u8 x300: {caps:?}");

  let mut b = BytesMut::new();
  let mut caps = vec![b.capacity()];
  for i in 0..300 {
    b.put_u8(i as u8);
    if *caps.last().unwrap() != b.capacity() {
      caps.push(b.capacity());
    }
  }
  println!("BytesMut  put_u8 x300: {caps:?}");

  // 直接走 chunk_mut:容量满时先 reserve(64)
  let mut v: Vec<u8> = Vec::new();
  let mut caps = vec![v.capacity()];
  for i in 0..300 {
    v.chunk_mut().write_byte(0, i as u8);
    // SAFETY: 上一行刚写了 chunk_mut 的第 0 个字节
    unsafe { v.advance_mut(1) };
    if *caps.last().unwrap() != v.capacity() {
      caps.push(v.capacity());
    }
  }
  println!("Vec<u8>   chunk_mut x300: {caps:?}");

  let mut b = BytesMut::new();
  let mut caps = vec![b.capacity()];
  for i in 0..300 {
    b.chunk_mut().write_byte(0, i as u8);
    // SAFETY: 同上
    unsafe { b.advance_mut(1) };
    if *caps.last().unwrap() != b.capacity() {
      caps.push(b.capacity());
    }
  }
  println!("BytesMut  chunk_mut x300: {caps:?}");

  println!("remaining_mut: Vec {} / BytesMut {} / isize::MAX {}",
    Vec::<u8>::new().remaining_mut(), BytesMut::new().remaining_mut(), isize::MAX);
  let mut arr = [0u8; 8];
  let mut s = &mut arr[..];
  s.put_u16(0xabcd);
  println!("&mut [u8; 8] after put_u16: remaining_mut {}", s.remaining_mut());
  println!("arr = {arr:?}");
}

问:put_u8 那两行的第一次扩容扩到多少?chunk_mut 那两行呢?

&mut [u8] 与 &mut [MaybeUninit<u8>]:写多少,切掉多少

固定大小的切片当 BufMut 用时,「写游标」就是切片本身的起点。advance_mut 把已经写过的前缀切掉:

    #[inline]
    unsafe fn advance_mut(&mut self, cnt: usize) {
        if self.len() < cnt {
            panic_advance(&TryGetError {
                requested: cnt,
                available: self.len(),
            });
        }

        // Lifetime dance taken from `impl Write for &mut [u8]`.
        let (_, b) = core::mem::take(self).split_at_mut(cnt);
        *self = b;
    }

mem::take(self) 先把 self 换成空切片,拿到原来那个 &'a mut [u8] 的所有权,split_at_mut 之后把后半段放回去。直接写 *self = &mut self[cnt..] 过不了借用检查:新切片的生命周期会被绑在对 self 的这次借用上,而不是原来的 'a。所以 BufMut 要实现在 &mut [u8] 这个引用上,调用方写 let mut s = &mut arr[..]; s.put_u16(..),写完 s 变短,arr 里是写好的数据。上面程序最后两行就是这个:remaining_mut 从 8 变成 6,arr 开头是 0xab, 0xcd。

&mut [u8] 已经初始化,advance_mut 标着 unsafe 其实没有什么需要担保的;它必须是 unsafe fn 只是因为 trait 这样声明。&mut [MaybeUninit<u8>] 的实现(PR #597,1.5.0)和它几乎逐行相同,区别是 chunk_mut 用 UninitSlice::uninit,put_slice 用 ptr::copy_nonoverlapping 写进 MaybeUninit。PR 的动机是「This will allow using BufMut without having to initialise memory first」,例子是把栈上的 [MaybeUninit::uninit(); 4096] 直接交给 tokio 的 read_buf。前面实验里 storage 就是这样用的。

动脑筋答案

Vec<u8>   put_u8 x300: [0, 8, 16, 32, 64, 128, 256, 512]
BytesMut  put_u8 x300: [0, 8, 16, 32, 64, 128, 256, 512]
Vec<u8>   chunk_mut x300: [0, 64, 128, 256, 512]
BytesMut  chunk_mut x300: [0, 64, 128, 256, 512]
remaining_mut: Vec 9223372036854775807 / BytesMut 9223372036854775807 / isize::MAX 9223372036854775807
&mut [u8; 8] after put_u16: remaining_mut 6
arr = [171, 205, 0, 0, 0, 0, 0, 0]

跑了 3 次,输出相同。put_u8 第一次扩到 8,chunk_mut 第一次扩到 64。

  • put_u8 不经过 chunk_mut:它调 put_slice(&[n]),两个类型都把 put_slice 覆盖成了 extend_from_slice,只要 1 个字节,于是走标准库 Vec 的倍增,u8 的最小非零容量是 8,之后 16、32……BytesMut 在 KIND_VEC 下把扩容交给 Vec::reserve,序列完全一样。
  • chunk_mut 写法在满的时候 reserve(64):从 0 开始,max(2 × 0, 0 + 64) 是 64;之后每次满了 2 × cap 都不小于 len + 64,所以是 128、256、512。少扩了 3 次(8、16、32),代价是第一次就要 64 字节。
  • remaining_mut 两个都是 isize::MAX(长度为 0 时),和容量无关。
  • &mut [u8; 8] 写了 put_u16(0xabcd) 之后剩 6,大端序写入,arr 前两个字节是 171(0xab)、205(0xcd)。

小结

  • BufMut 必需 remaining_mut、chunk_mut、advance_mut 三个方法,trait 本身是 unsafe trait(0.6.0,#432):bytes 里的 unsafe 代码信任实现给出的长度。
  • chunk_mut 返回 &mut UninitSlice:只许写、不许读,也不许写入未初始化的值;没有 Index<usize>,读的写法编译不过。返回类型从 &mut [u8](0.4)到 &mut [MaybeUninit<u8>](0.5.0)再到 UninitSlice(0.6.0,issue #328)。
  • advance_mut 是 unsafe:越界能查、会 panic,「这些字节写过没有」查不了,只能由调用方担保。as_uninit_slice_mut 是唯一的 unsafe 出口,用来接 tokio 的 ReadBuf::uninit,hyper 和 tokio 的 read_buf 都这样用。
  • Vec<u8> 和 BytesMut 的 remaining_mut 是 isize::MAX - len(分别自 1.1.0 和 1.11.0),表示最多还能长多少,不是剩余容量。
  • 默认的 put_slice 是「chunk_mut、拷贝、advance_mut」的循环,所有 put_* 都经过它;Vec 和 BytesMut 把它覆盖成 extend_from_slice,所以 put_u8 按标准库的倍增扩容(8、16、32……),直接用 chunk_mut 时满了才 reserve(64)(64、128、256……)。
  • BytesMut::put 在自身容量为 0 时用 copy_to_bytes + try_into_mut 接管源的缓冲,唯一拥有的 Bytes 0 次分配;源为空时先返回,防止 copy_to_bytes 与 put 互相递归(1.11.0,#793、#794、#803)。
  • &mut [u8] 和 &mut [MaybeUninit<u8>] 把切片本身当游标,advance_mut 切掉已写的前缀;后者(1.5.0)让栈上的缓冲不用先清零。

💬 关联讨论

发起讨论
加载中...