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>] 布局相同,所以几个构造函数都只是一次指针转换。
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 每次读多少见读写缓冲。
问: 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 收一个唯一的 Bytes | 0 次分配,same buffer: 1 | split_to(len) 交出整段,try_into_mut 成功,dst 直接用源的缓冲 |
同样的数据用 put_slice | 1 次分配 | extend_from_slice,分配 4096 字节再拷贝 |
| 源还有别的 clone | 1 次分配,不同缓冲 | try_into_mut 返回 Err,退回 extend_from_slice |
源是 from_static | 1 次分配 | 静态存储的 try_into_mut 总是 Err |
| 目标已有 1 字节容量 | with_capacity 1 次 + reserve 1 次 realloc | 容量不是 0,走 else 分支照常拷贝 |
Vec<u8> 收 Bytes | 1 次分配 | Vec 的 put 没有这条复用分支 |
空 BytesMut 收 &[u8] | 1 次分配 | 默认 copy_to_bytes 分配并拷贝一次,try_into_mut 拿回同一块 |
三段 Chain | 1 次 realloc,容量 3000 | put 先 reserve(3000) |
同样三段分三次 put_slice | 3 次 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。
#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_mut | chunk_mut | advance_mut | 覆盖的 put_* |
|---|---|---|---|---|
Vec<u8> | isize::MAX - len | 满了先 reserve(64),交出 len..cap 这一段 | set_len,超过容量 panic | put、put_slice(extend_from_slice)、put_bytes(resize) |
BytesMut | isize::MAX - len | 满了先 reserve(64),交出 spare_capacity_mut() | len += cnt,超过容量 panic | put、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
#[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。
平台 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接管源的缓冲,唯一拥有的Bytes0 次分配;源为空时先返回,防止copy_to_bytes与put互相递归(1.11.0,#793、#794、#803)。&mut [u8]和&mut [MaybeUninit<u8>]把切片本身当游标,advance_mut切掉已写的前缀;后者(1.5.0)让栈上的缓冲不用先清零。