data 字段的编码
主线位置:构建主线的地基。
BytesMut的每一步(写入、advance、split_to、reserve、freeze、drop)都先看data的最低位,决定按「还是一个Vec」还是按「和别人共享一个Shared」处理。这一页只讲这个字怎么编码、什么时候从一种形态变成另一种;split 和 freeze 的完整流程在 split 与 freeze,reserve的三种结果在 reserve。
stateDiagram-v2 state "KIND_VEC:data 是一组位" as KindVec state "KIND_ARC:data 指向 Shared" as KindArc [*] --> KindVec : from_vec KindVec --> KindVec : advance 改 pos KindVec --> KindArc : split 或偏移溢出 KindArc --> KindArc : split 只加计数 KindArc --> KindVec : reserve 另分配 KindVec --> [*] : freeze 或 drop KindArc --> [*] : freeze 或 drop
BytesMut 只有四个字,32 字节(大小的实测见 结构与 Vtable):
pub struct BytesMut {
ptr: NonNull<u8>,
len: usize,
cap: usize,
data: *mut Shared,
}
ptr、len、cap 描述的是这个句柄看得到的那一段:从 ptr 开始,已写 len 字节,最多能写到 cap。它们不一定是底层 Vec 的起点和容量:前面被 advance 或 split_to 切掉的部分不算在里面。要把底层的 Vec 还原出来(drop、freeze、reserve 都要),就得知道前面切掉了多少,还得在共享时知道引用计数在哪里。这两件事都塞进了第四个字 data。它的类型写成 *mut Shared,但只有一半时间真的是指针。
最低位:KIND_VEC 与 KIND_ARC
// Buffer storage strategy flags.
const KIND_ARC: usize = 0b0;
const KIND_VEC: usize = 0b1;
const KIND_MASK: usize = 0b1;
#[inline]
fn kind(&self) -> usize {
self.data as usize & KIND_MASK
}
KIND_VEC(最低位 1):这个BytesMut独占一个Vec<u8>,从没被 split 过。data不是指针,是一组位,下一节拆开。KIND_ARC(最低位 0):data是一个真指针,指向bytes_mut.rs自己的Shared:
struct Shared {
vec: Vec<u8>,
original_capacity_repr: usize,
ref_count: AtomicUsize,
}
Shared 是 Box 分配的,地址按它的对齐排;只要对齐是偶数,地址的最低位就一定是 0,不用额外打标记,指针本身就是 KIND_ARC。这条前提由一个编译期断言守着(源码):
// Assert that the alignment of `Shared` is divisible by 2.
// This is a necessary invariant since we depend on allocating `Shared` a
// shared object to implicitly carry the `KIND_ARC` flag in its pointer.
// This flag is set when the LSB is 0.
const _: [(); 0 - mem::align_of::<Shared>() % 2] = []; // Assert that the alignment of `Shared` is divisible by 2.
0 - 1 在常量求值时溢出、编译失败的写法,和 bytes.rs 里那份一样,见 五种存储。Shared 里有 Vec 和两个 usize,64 位上对齐是 8、大小 40 字节;下面的实验在 wasm32 上量到 20 字节。
两个文件各有一个 Shared,都靠「对齐至少为 2,最低位必为 0」当 KIND_ARC 标记。PR #652(「BytesMut: Assert alignment of Shared」)的说明是:「Back in #362, an assertion was added to ensure that the alignment of bytes::Shared is even so we can use the least significant bit as a flag. bytes_mut::Shared uses the same technique but has no such assertion so I've added one here.」
之前 bytes_mut.rs 只在 promote_to_shared 里有一句运行期的 debug_assert_eq!(shared as usize & KIND_MASK, KIND_ARC),release 构建不检查;之后多了上面这行常量,任何构建都在编译期检查。这个 PR 只加了 6 行,没有改任何行为,首次发布在 1.6.0,CHANGELOG 记为「BytesMut: Assert alignment of Shared (#652)」。
KIND_VEC 时的其余位
KIND_VEC 时 data 的布局由这几个常量决定:
// The max original capacity value. Any `Bytes` allocated with a greater initial
// capacity will default to this.
const MAX_ORIGINAL_CAPACITY_WIDTH: usize = 17;
// The original capacity algorithm will not take effect unless the originally
// allocated capacity was at least 1kb in size.
const MIN_ORIGINAL_CAPACITY_WIDTH: usize = 10;
// The original capacity is stored in powers of 2 starting at 1kb to a max of
// 64kb. Representing it as such requires only 3 bits of storage.
const ORIGINAL_CAPACITY_MASK: usize = 0b11100;
const ORIGINAL_CAPACITY_OFFSET: usize = 2;
const VEC_POS_OFFSET: usize = 5;
// When the storage is in the `Vec` representation, the pointer can be advanced
// at most this value. This is due to the amount of storage available to track
// the offset is usize - number of KIND bits and number of ORIGINAL_CAPACITY
// bits.
const MAX_VEC_POS: usize = usize::MAX >> VEC_POS_OFFSET;
const NOT_VEC_POS_MASK: usize = 0b11111;
画成一张表(64 位):
| 位 | 掩码 / 偏移 | 内容 |
|---|---|---|
| 0 | KIND_MASK = 0b1 | 1,即 KIND_VEC |
| 1 | — | 不用,始终是 0 |
| 2–4 | ORIGINAL_CAPACITY_MASK = 0b11100,ORIGINAL_CAPACITY_OFFSET = 2 | 原始容量的编码 repr,0–7 |
| 5–63 | VEC_POS_OFFSET = 5 | pos:ptr 相对 Vec 起点前进了多少字节,最大 MAX_VEC_POS |
NOT_VEC_POS_MASK = 0b11111 是低 5 位,改 pos 时用它保住 kind 和 repr。第 1 位在整个 bytes_mut.rs 里没有任何代码读写,ORIGINAL_CAPACITY_OFFSET 却是 2,不是 1。
0.5.0 之前,Bytes 和 BytesMut 共用一个 Inner,kind 有四种,要占两位(0.4.12 的 src/bytes.rs):
const KIND_ARC: usize = 0b00;
const KIND_INLINE: usize = 0b01;
const KIND_STATIC: usize = 0b10;
const KIND_VEC: usize = 0b11;
const KIND_MASK: usize = 0b11;
KIND_INLINE 是把短数据直接放在结构体里,KIND_STATIC 是 'static 切片。PR #298(「Refactor Bytes to use an internal vtable」,0.5.0)把 Bytes 改成 vtable,提交说明里写「Removed the "inline" (SBO) mechanism in Bytes」,静态切片也改由 Bytes 的 STATIC_VTABLE 承担(见 结构与 Vtable)。新建的 bytes_mut.rs 只剩两种 kind,KIND_MASK 变成 0b1,但 ORIGINAL_CAPACITY_MASK = 0b11100、VEC_POS_OFFSET = 5 这几行原样搬了过来。于是第 1 位从此空着。MAX_ORIGINAL_CAPACITY_WIDTH 上面那句「Any Bytes allocated with a greater initial capacity will default to this」也是那时一起搬来的,现在这个常量只给 BytesMut 用。
原始容量:3 位记一个 2 的幂
「原始容量」是这个 BytesMut 最初拿到的缓冲有多大。它只在一个地方用:reserve 发现缓冲还被别人共享、只能另分配时,新容量至少取这么大(reserve_inner 的最后一段,见 reserve)。用途只是一个下限,不需要精确,所以只记它落在哪个 2 的幂区间。original_capacity_to_repr 和 original_capacity_from_repr:
#[inline]
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))
}
编码:先右移 10 位(除以 1024),再数剩下的数有几位有效位(PTR_WIDTH - leading_zeros),最多取 7(17 - 10)。小于 1024 的容量右移后是 0,有效位 0,repr = 0。解码:repr 为 0 还原成 0;否则还原成 2^(repr + 9),也就是这个区间的下界。结果:
| 原始容量 | cap >> 10 | repr | 还原值 |
|---|---|---|---|
| 0 – 1023 | 0 | 0 | 0 |
| 1024 – 2047 | 1 | 1 | 1024 |
| 2048 – 4095 | 2–3 | 2 | 2048 |
| 4096 – 8191 | 4–7 | 3 | 4096 |
| 8192 – 16383 | 8–15 | 4 | 8192 |
| 16384 – 32767 | 16–31 | 5 | 16384 |
| 32768 – 65535 | 32–63 | 6 | 32768 |
| 65536 及以上 | 64 及以上 | 7 | 65536 |
还原值总是向下取到 2 的幂,最大 64 KiB。下面的实验会把每一行的 repr 从 data 里读出来,并让 reserve 实际用一次还原值。
问: MAX_ORIGINAL_CAPACITY_WIDTH 是 17,2 的 17 次方是 128 KiB,为什么注释和上表都说最大是 64 KiB?
答: 这两个常量说的是「位宽」,不是指数。repr 记的是容量有几位有效位减去 10,解码时取「这么多位的数里最小的那个」,即 2^(位宽 - 1)。位宽 17 的数在 65536 到 131071 之间,最小的是 2^16 = 65536。同样,MIN_ORIGINAL_CAPACITY_WIDTH = 10 对应的是位宽 11 的最小值 1024:位宽 10 及以下(小于 1024)的容量全部记成 0,按注释的话是「will not take effect」。
PTR_WIDTH 只在 original_capacity_to_repr 里用到,按目标平台的指针宽度取 64 或 32:
#[cfg(target_pointer_width = "64")]
const PTR_WIDTH: usize = 64;
#[cfg(target_pointer_width = "32")]
const PTR_WIDTH: usize = 32;
leading_zeros 数的是 usize 里前导零的个数,要用 usize 的位数去减才得到有效位数,所以这个常量必须和指针宽度一致(作用等同于 usize::BITS)。编码出来的 repr 与平台无关:实验在 wasm32 上跑,这张表一行不差。
repr 只在 from_vec 里算一次:
#[inline]
pub(crate) fn from_vec(vec: Vec<u8>) -> BytesMut {
let mut vec = ManuallyDrop::new(vec);
let ptr = vptr(vec.as_mut_ptr());
let len = vec.len();
let cap = vec.capacity();
let original_capacity_repr = original_capacity_to_repr(cap);
let data = (original_capacity_repr << ORIGINAL_CAPACITY_OFFSET) | KIND_VEC;
BytesMut {
ptr,
len,
cap,
data: invalid_ptr(data),
}
}
with_capacity、new(就是 with_capacity(0))、zeroed、From<&[u8]>、FromIterator,以及 Bytes 转回 BytesMut 的几条路,都经过这里。之后 repr 只被搬运,从不重算:提升时抄进 Shared::original_capacity_repr,reserve 另分配回到 KIND_VEC 时再抄回 data。KIND_VEC 形态下的 reserve 交给 Vec::reserve 扩容,cap 变了,repr 不变。所以一个从 with_capacity(0) 开始、靠 reserve 长大的缓冲,repr 永远是 0,后面实验的第 4 段会看到。
invalid_ptr(addr) 用 null_mut().wrapping_add(addr) 造一个「地址是这个整数、但不指向任何东西」的指针,而不是写 addr as *mut _:后者在 miri 的严格来源(strict provenance)检查下会报错,这是 PR #553(1.2.0)改的。
偏移:pos 与 MAX_VEC_POS
KIND_VEC 时,advance 和 split_to 都要把 ptr 往后挪。ptr 挪了,Vec 的起点却还在原处,释放时必须把起点还给分配器,所以要记下挪了多少。get_vec_pos 和 set_vec_pos:
#[inline]
unsafe fn get_vec_pos(&self) -> usize {
debug_assert_eq!(self.kind(), KIND_VEC);
self.data as usize >> VEC_POS_OFFSET
}
#[inline]
unsafe fn set_vec_pos(&mut self, pos: usize) {
debug_assert_eq!(self.kind(), KIND_VEC);
debug_assert!(pos <= MAX_VEC_POS);
self.data = invalid_ptr((pos << VEC_POS_OFFSET) | (self.data as usize & NOT_VEC_POS_MASK));
}
读出 pos 的地方都是为了还原 Vec。rebuild_vec 把切掉的前缀加回去:
unsafe fn rebuild_vec(ptr: *mut u8, mut len: usize, mut cap: usize, off: usize) -> Vec<u8> {
let ptr = ptr.sub(off);
len += off;
cap += off;
Vec::from_raw_parts(ptr, len, cap)
}
Drop、freeze、From<BytesMut> for Vec<u8>、reserve_inner 的 KIND_VEC 分支和 promote_to_shared 都先取 pos,再调它。调用 set_vec_pos 的只有两处:前进时的 advance_unchecked,和 reserve_inner 把数据挪回开头后 set_vec_pos(0)。
前进在 advance_unchecked,Buf::advance 和 split_to 都走它:
pub(crate) unsafe fn advance_unchecked(&mut self, count: usize) {
// Setting the start to 0 is a no-op, so return early if this is the
// case.
if count == 0 {
return;
}
debug_assert!(count <= self.cap, "internal: set_start out of bounds");
let kind = self.kind();
if kind == KIND_VEC {
// ...
let pos = self.get_vec_pos() + count;
if pos <= MAX_VEC_POS {
self.set_vec_pos(pos);
} else {
// The repr must be upgraded to ARC. This will never happen
// on 64 bit systems and will only happen on 32 bit systems
// when shifting past 134,217,727 bytes. As such, we don't
// worry too much about performance here.
self.promote_to_shared(/*ref_count = */ 1);
}
}
// Updating the start of the view is setting `ptr` to point to the
// new start and updating the `len` field to reflect the new length
// of the view.
self.ptr = vptr(self.ptr.as_ptr().add(count));
self.len = self.len.saturating_sub(count);
self.cap -= count;
}
pos 占 PTR_WIDTH - 5 位:
| 平台 | MAX_VEC_POS | 约合 |
|---|---|---|
| 64 位 | 2^59 - 1 = 576460752303423487 | 512 PiB,没有哪块分配有这么大 |
| 32 位 | 2^27 - 1 = 134217727 | 128 MiB,一个大文件缓冲就能碰到 |
位不够时不报错,也不截断,而是换一种不需要记偏移的形态:promote_to_shared(1) 把 Vec 装进一个计数为 1 的 Shared,data 变成指针。KIND_ARC 下偏移不存在 data 里,需要时用 ptr 减去 Shared::vec 的起点现算(reserve_inner 里的 self.ptr.as_ptr().offset_from(ptr)),所以多大的偏移都表示得了。代价是一次 Shared 的分配,注释说这条路「we don't worry too much about performance here」。后面的实验在 wasm32 上把这条路走了一遍。
KIND_ARC 时 advance_unchecked 只改 ptr、len、cap,不碰 data。
提升:promote_to_shared
从 KIND_VEC 变成 KIND_ARC 只有 promote_to_shared 一个入口:
unsafe fn promote_to_shared(&mut self, ref_cnt: usize) {
debug_assert_eq!(self.kind(), KIND_VEC);
debug_assert!(ref_cnt == 1 || ref_cnt == 2);
let original_capacity_repr =
(self.data as usize & ORIGINAL_CAPACITY_MASK) >> ORIGINAL_CAPACITY_OFFSET;
// The vec offset cannot be concurrently mutated, so there
// should be no danger reading it.
let off = (self.data as usize) >> VEC_POS_OFFSET;
// ...
// Explicitly allocate before invoking rebuild_vec() so that
// the vector is not dropped if Box::new() panics.
let shared = Box::new(MaybeUninit::<Shared>::uninit());
let shared = Shared::init_to_raw(
shared,
Shared {
vec: rebuild_vec(self.ptr.as_ptr(), self.len, self.cap, off),
original_capacity_repr,
ref_count: AtomicUsize::new(ref_cnt),
},
);
// The pointer should be aligned, so this assert should
// always succeed.
debug_assert_eq!(shared as usize & KIND_MASK, KIND_ARC);
self.data = shared;
}
它把 data 里的两样东西分别安顿好:repr 抄进 Shared::original_capacity_repr(data 变成指针后没地方放了);pos 用来 rebuild_vec,还原出完整的 Vec 放进 Shared::vec,之后就不再需要。ptr、len、cap 一个都不改,句柄看到的那一段不变。先 Box::new 再 rebuild_vec 的顺序是 1.12.1 唯一的修复(#837),见 怎么读这一卷。
ref_cnt 只有两种取值:
- 2:
shallow_clone在KIND_VEC时调用,提升完马上ptr::read(self)复制出第二个句柄,所以计数直接从 2 开始。split_to、split_off、split都经过它,连split_to(0)也一样会提升(见文末的动脑筋)。 - 1:上一节的偏移溢出,只有自己一个句柄。
反方向,从 KIND_ARC 回到 KIND_VEC,只发生在 reserve_inner 的最后一段:缓冲还被别人共享,只能新分配一个 Vec,拷贝数据、release_shared 放掉旧的那份计数,然后
let data = (original_capacity_repr << ORIGINAL_CAPACITY_OFFSET) | KIND_VEC;
self.data = invalid_ptr(data);
repr 从 Shared 抄回来,pos 是 0。其余情况(唯一持有时 reserve 原地复用;freeze 出来的 Bytes 唯一时转回 BytesMut,拿回的还是同一个 Shared)都留在 KIND_ARC。
实验:把 data 的位打印出来
公开 API 看不到 data。下面的程序把 BytesMut 当成 4 个 usize 读出来。这依赖私有布局,只用于观察:先断言 size_of::<BytesMut>() 等于 4 个字;字段顺序由编译器决定,所以先用一个 with_capacity(3000) 再写 5 字节的缓冲定位(ptr、len、cap 和 as_ptr()、len()、capacity() 对上,剩下的那个字是 data),之后每次读都再校验这三个字。KIND_ARC 时 data 是真地址,每次运行不同,只打印低 3 位。分配计数用的是包住 System 的计数分配器,和前几页一样,这里不重复。
bytes = "=1.12.1"
mirror.rs(省略了计数分配器):
use std::mem::size_of;
use bytes::{BufMut, Bytes, BytesMut};
// ---- 只用于观察:把 BytesMut / Bytes 当成 4 个 usize 读(依赖私有布局)----
pub struct Layout4 {
ptr: usize,
len: usize,
cap: usize,
data: usize,
}
fn words_mut(b: &BytesMut) -> [usize; 4] {
assert_eq!(size_of::<[usize; 4]>(), size_of::<BytesMut>());
unsafe { *(b as *const BytesMut as *const [usize; 4]) }
}
pub fn words_bytes(b: &Bytes) -> [usize; 4] {
assert_eq!(size_of::<[usize; 4]>(), size_of::<Bytes>());
unsafe { *(b as *const Bytes as *const [usize; 4]) }
}
pub fn locate() -> Layout4 {
let mut b = BytesMut::with_capacity(3000);
b.put_slice(b"hello");
let w = words_mut(&b);
let ptr = w.iter().position(|&x| x == b.as_ptr() as usize).unwrap();
let len = w.iter().position(|&x| x == b.len()).unwrap();
let cap = w.iter().position(|&x| x == b.capacity()).unwrap();
let data = (0..4).find(|&i| i != ptr && i != len && i != cap).unwrap();
Layout4 { ptr, len, cap, data }
}
pub fn data_of(l: &Layout4, b: &BytesMut) -> usize {
let w = words_mut(b);
assert_eq!(w[l.ptr], b.as_ptr() as usize);
assert_eq!(w[l.len], b.len());
assert_eq!(w[l.cap], b.capacity());
w[l.data]
}
// 照 bytes_mut.rs 的常量拆 data
pub fn decode(d: usize) -> String {
if d & 1 == 1 {
let bit1 = (d >> 1) & 1;
let repr = (d & 0b11100) >> 2;
let pos = d >> 5;
format!("KIND_VEC bit1={bit1} repr={repr} pos={pos}")
} else {
format!("KIND_ARC Shared 指针 (d & 0b111 = {})", d & 0b111)
}
}
// 低位用二进制打出来;KIND_ARC 时是真地址,每次运行不同,只打低 3 位
pub fn show(d: usize) -> String {
if d & 1 == 1 { format!("{d:#b}") } else { format!("...{:03b}", d & 0b111) }
}
bits.rs 分四段:各种 with_capacity(n) 的 repr,并让共享路径的 reserve 用一次还原值;一块 8 KiB 缓冲上的 advance、split_to、freeze、reserve;带偏移的 KIND_VEC 缓冲 freeze;with_capacity(0) 加 reserve 和直接 with_capacity 的对比。
use bytes::{Buf, BufMut, BytesMut};
#[path = "../mirror.rs"]
mod mirror;
use mirror::*;
// 照抄 original_capacity_from_repr 的公式,只用来和观察值对照
fn from_repr(repr: usize) -> usize {
if repr == 0 { 0 } else { 1 << (repr + 9) }
}
fn main() {
let l = locate();
println!("== 1. with_capacity(n) 之后的 data,以及共享路径 reserve 用到的原始容量");
for n in [0usize, 1, 1023, 1024, 1500, 2047, 2048, 3000, 4096, 8192, 65535, 65536, 131072, 1 << 20] {
let mut b = BytesMut::with_capacity(n);
let d = data_of(&l, &b);
let repr = (d & 0b11100) >> 2;
// 填满、整段切走并留着,b 变成 KIND_ARC 且不唯一;
// 这时 reserve(1) 只能另分配 max(0 + 1, 原始容量)
let c = b.capacity();
b.put_bytes(0, c);
let keep = b.split_to(c);
b.reserve(1);
println!(
"n={n:<8} data={:<10} repr={repr} 公式还原={:<6} 共享后 reserve(1) 得到 cap={}",
show(d),
from_repr(repr),
b.capacity()
);
drop(keep);
}
println!();
println!("== 2. 一块 8192 字节缓冲的一串操作");
let mut b = BytesMut::with_capacity(8192);
let base = b.as_ptr() as usize;
b.put_slice(&[7u8; 100]);
let d = data_of(&l, &b);
println!("put_slice(100) data={:<16} {} ptr=base+{} len={} cap={}", show(d), decode(d), b.as_ptr() as usize - base, b.len(), b.capacity());
b.advance(10);
let d = data_of(&l, &b);
println!("advance(10) data={:<16} {} ptr=base+{} len={} cap={}", show(d), decode(d), b.as_ptr() as usize - base, b.len(), b.capacity());
b.advance(20);
let d = data_of(&l, &b);
println!("advance(20) data={:<16} {} ptr=base+{} len={} cap={}", show(d), decode(d), b.as_ptr() as usize - base, b.len(), b.capacity());
let (a0, s0) = allocs();
let head = b.split_to(20);
let (a1, s1) = allocs();
let dh = data_of(&l, &head);
let db = data_of(&l, &b);
println!("split_to(20) alloc+{} ({}B) head: {} b: {} 同一个 Shared={}", a1 - a0, s1 - s0, decode(dh), decode(db), dh == db);
println!(" head: ptr=base+{} len={} cap={} | b: ptr=base+{} len={} cap={}",
head.as_ptr() as usize - base, head.len(), head.capacity(), b.as_ptr() as usize - base, b.len(), b.capacity());
let (a0, _) = allocs();
let head = head.freeze();
let (a1, _) = allocs();
let w = words_bytes(&head);
println!("head.freeze() alloc+{} Bytes 的 4 个字里有原来的 Shared 指针={} ptr=base+{} len={}",
a1 - a0, w.contains(&dh), head.as_ptr() as usize - base, head.len());
let (a0, s0) = allocs();
b.reserve(10000);
let (a1, s1) = allocs();
let d = data_of(&l, &b);
println!("b.reserve(10000) alloc+{} ({}B) data={} {} ptr 在原缓冲内={} len={} cap={}",
a1 - a0, s1 - s0, show(d), decode(d), (b.as_ptr() as usize).wrapping_sub(base) < 8192, b.len(), b.capacity());
drop(head);
println!();
println!("== 3. KIND_VEC 带偏移的 freeze");
let mut v = BytesMut::with_capacity(8192);
let base = v.as_ptr() as usize;
v.put_slice(&[7u8; 100]);
v.advance(10);
let d = data_of(&l, &v);
let (a0, s0) = allocs();
let f = v.freeze();
let (a1, s1) = allocs();
println!("freeze 前 data={} {}", show(d), decode(d));
println!("freeze 后 alloc+{} ({}B) Bytes ptr=base+{} len={}", a1 - a0, s1 - s0, f.as_ptr() as usize - base, f.len());
drop(f);
println!();
println!("== 4. 先 with_capacity(0) 再 reserve,和直接 with_capacity");
for (label, mut b) in [
("with_capacity(0)+reserve(65536)", {
let mut b = BytesMut::with_capacity(0);
b.reserve(65536);
b
}),
("with_capacity(65536)", BytesMut::with_capacity(65536)),
] {
let d = data_of(&l, &b);
let c = b.capacity();
b.put_bytes(0, c);
let keep = b.split_to(c);
b.reserve(10);
println!("{label:<32} cap={c} data={:<10} {} 切走全部后 reserve(10) cap={}", show(d), decode(d), b.capacity());
drop(keep);
}
}
M2 Max、rustc 1.99.0,cargo run --release --bin bits,跑 3 次,输出相同:
== 1. with_capacity(n) 之后的 data,以及共享路径 reserve 用到的原始容量
n=0 data=0b1 repr=0 公式还原=0 共享后 reserve(1) 得到 cap=1
n=1 data=0b1 repr=0 公式还原=0 共享后 reserve(1) 得到 cap=1
n=1023 data=0b1 repr=0 公式还原=0 共享后 reserve(1) 得到 cap=1
n=1024 data=0b101 repr=1 公式还原=1024 共享后 reserve(1) 得到 cap=1024
n=1500 data=0b101 repr=1 公式还原=1024 共享后 reserve(1) 得到 cap=1024
n=2047 data=0b101 repr=1 公式还原=1024 共享后 reserve(1) 得到 cap=1024
n=2048 data=0b1001 repr=2 公式还原=2048 共享后 reserve(1) 得到 cap=2048
n=3000 data=0b1001 repr=2 公式还原=2048 共享后 reserve(1) 得到 cap=2048
n=4096 data=0b1101 repr=3 公式还原=4096 共享后 reserve(1) 得到 cap=4096
n=8192 data=0b10001 repr=4 公式还原=8192 共享后 reserve(1) 得到 cap=8192
n=65535 data=0b11001 repr=6 公式还原=32768 共享后 reserve(1) 得到 cap=32768
n=65536 data=0b11101 repr=7 公式还原=65536 共享后 reserve(1) 得到 cap=65536
n=131072 data=0b11101 repr=7 公式还原=65536 共享后 reserve(1) 得到 cap=65536
n=1048576 data=0b11101 repr=7 公式还原=65536 共享后 reserve(1) 得到 cap=65536
== 2. 一块 8192 字节缓冲的一串操作
put_slice(100) data=0b10001 KIND_VEC bit1=0 repr=4 pos=0 ptr=base+0 len=100 cap=8192
advance(10) data=0b101010001 KIND_VEC bit1=0 repr=4 pos=10 ptr=base+10 len=90 cap=8182
advance(20) data=0b1111010001 KIND_VEC bit1=0 repr=4 pos=30 ptr=base+30 len=70 cap=8162
split_to(20) alloc+1 (40B) head: KIND_ARC Shared 指针 (d & 0b111 = 0) b: KIND_ARC Shared 指针 (d & 0b111 = 0) 同一个 Shared=true
head: ptr=base+30 len=20 cap=20 | b: ptr=base+50 len=50 cap=8142
head.freeze() alloc+0 Bytes 的 4 个字里有原来的 Shared 指针=true ptr=base+30 len=20
b.reserve(10000) alloc+1 (10050B) data=0b10001 KIND_VEC bit1=0 repr=4 pos=0 ptr 在原缓冲内=false len=50 cap=10050
== 3. KIND_VEC 带偏移的 freeze
freeze 前 data=0b101010001 KIND_VEC bit1=0 repr=4 pos=10
freeze 后 alloc+1 (24B) Bytes ptr=base+10 len=90
== 4. 先 with_capacity(0) 再 reserve,和直接 with_capacity
with_capacity(0)+reserve(65536) cap=65536 data=0b1 KIND_VEC bit1=0 repr=0 pos=0 切走全部后 reserve(10) cap=10
with_capacity(65536) cap=65536 data=0b11101 KIND_VEC bit1=0 repr=7 pos=0 切走全部后 reserve(10) cap=65536
第 1 段和上面的表一行一行对得上:repr 是 data 的第 2–4 位,0b1101 就是 repr = 3 加 KIND_VEC。每一行最后一列是真实行为:缓冲整段切给 keep 之后,b 是 KIND_ARC、不唯一,reserve(1) 只能另分配 max(0 + 1, 原始容量),得到的容量正好是还原值。3000 字节的缓冲还原成 2048,65535 还原成 32768(差 1 字节就掉一档),1 MiB 只还原成 64 KiB。整段的第 1 位都是 0。
第 2 段:
advance(10)之后data = 0b101010001:低 5 位10001没动(KIND_VEC,repr = 4),高位1010是 10。再advance(20),高位变成11110= 30。ptr随之是base+30,cap每次减去前进的字节数。split_to(20)分配 40 字节的Shared,两个句柄的data是同一个指针,最低位 0。pos从data里消失了,但head的ptr仍是base+30:偏移现在由ptr和Shared::vec的起点之差表示。head.freeze()不分配,Bytes的 4 个字里能找到同一个Shared地址,KIND_ARC的 freeze 就是把这个指针原样交给Bytes(细节在 split 与 freeze)。b.reserve(10000)时head还活着,b不唯一,只能另分配:新容量是max(50 + 10000, 8192)= 10050,data回到0b10001,repr = 4是从Shared里抄回来的,pos = 0。
第 3 段:带偏移 10 的 KIND_VEC 缓冲 freeze,rebuild_vec 先把前缀加回去,交给 From<Vec<u8>>(长度不等于容量,分配 24 字节的 bytes.rs Shared,见 五种存储),再 advance(10),所以 Bytes 的 ptr 是 base+10。pos 在这里变成了 Bytes 的 ptr 偏移。
第 4 段是下一节的内容。
32 位上的偏移溢出
64 位上走不到 promote_to_shared(1) 那条路。32 位上只要 128 MiB,可以真跑一次。vecpos.rs 分配 MAX_VEC_POS + 2 字节,先前进 MAX_VEC_POS,再前进 1:
use std::fmt::Write;
use bytes::{Buf, BytesMut};
#[path = "../mirror.rs"]
mod mirror;
use mirror::*;
fn main() {
// 输出先攒在 String 里,最后一次打印
let mut out = String::with_capacity(4096);
let l = locate();
let max_vec_pos = usize::MAX >> 5; // MAX_VEC_POS
writeln!(out, "usize = {} 位, MAX_VEC_POS = {max_vec_pos}", usize::BITS).unwrap();
if usize::BITS == 32 {
// 分配 MAX_VEC_POS + 2 字节,先前进到 MAX_VEC_POS,再前进 1
let mut b = BytesMut::zeroed(max_vec_pos + 2);
let d = data_of(&l, &b);
writeln!(out, "zeroed({}) data={} {}", max_vec_pos + 2, show(d), decode(d)).unwrap();
b.advance(max_vec_pos);
let d = data_of(&l, &b);
writeln!(out, "advance(MAX_VEC_POS) data={:#x} {} len={}", d, decode(d), b.len()).unwrap();
let (a0, s0) = allocs();
b.advance(1);
let (a1, s1) = allocs();
let d = data_of(&l, &b);
writeln!(out, "advance(1) alloc+{} ({}B) {} len={}", a1 - a0, s1 - s0, decode(d), b.len()).unwrap();
} else {
writeln!(out, "64 位上偏移要超过 {max_vec_pos} 字节才会走提升这条路,跳过").unwrap();
}
print!("{out}");
}
本机(64 位):
usize = 64 位, MAX_VEC_POS = 576460752303423487
64 位上偏移要超过 576460752303423487 字节才会走提升这条路,跳过
32 位用 --target wasm32-wasip1 编译,在 Node 22.23.3 自带的 WASI 下运行(输出攒到最后一次打印,是因为这个 Node 版本在内存长到一百多 MiB 后多次写 stderr 会崩溃,和 bytes 无关),跑 3 次,输出相同:
usize = 32 位, MAX_VEC_POS = 134217727
zeroed(134217729) data=0b11101 KIND_VEC bit1=0 repr=7 pos=0
advance(MAX_VEC_POS) data=0xfffffffd KIND_VEC bit1=0 repr=7 pos=134217727 len=2
advance(1) alloc+1 (20B) KIND_ARC Shared 指针 (d & 0b111 = 0) len=1
前进到 MAX_VEC_POS 时 data 是 0xfffffffd:高 27 位全满,低 5 位仍是 11101。再前进 1 字节,pos 放不下,advance_unchecked 调 promote_to_shared(1),分配一个 20 字节的 Shared(32 位上 Vec 是 12 字节,加两个 4 字节的字段),data 变成指针。缓冲本身没动,len 照常减 1。bits 在 wasm32 上也跑过,第 1 段的表完全相同,只有第 2、3 段里两种 Shared 的大小变成 20 和 12 字节。
原始容量会丢:先 with_capacity(0) 再 reserve
第 4 段的两个缓冲容量都是 65536,data 却不同:直接 with_capacity(65536) 的 repr = 7;with_capacity(0) 再 reserve(65536) 的 repr = 0。原因在前面:repr 只在 from_vec 里按当时的容量算一次,那时容量是 0;之后 KIND_VEC 的 reserve 交给 Vec::reserve,不会回头改 repr。
差别在缓冲被共享、需要另分配时才显出来。两个缓冲都整段切走、留着切出去的那段,再 reserve(10):前者只拿到 10 字节,后者拿到 65536 字节。原始容量本来就是为了这个场景,让一个读缓冲在把数据切给别人之后,下一次分配仍按原来的规模来,而不是按这一次碰巧要的字节数。
hyper 的读缓冲正好是先 with_capacity(0) 再 reserve(Buffered::new,过程见 一块缓冲的一生),所以它的 repr 是 0,原始容量这套机制在它身上不起作用。它每次 reserve 的参数本身就是下一次读的大小(初始 8 KiB),不靠这个下限。自己写读缓冲时,如果每次 reserve 的量很小(比如只按一帧剩下的字节数),从 with_capacity(0) 起步就会在共享之后反复小块分配;reserve 的完整规则在 reserve,这个坑在「常见坑」一章还会单独复现。
平台 M2 Max、rustc 1.99.0,mirror.rs 同上。先在纸上算出 data 的十六进制值和 capacity();再想一想,在这之后 split_to(0) 切出一个空的 BytesMut,会不会分配?
use bytes::{Buf, BufMut, BytesMut};
#[path = "../mirror.rs"]
mod mirror;
use mirror::*;
fn main() {
let l = locate();
let mut b = BytesMut::with_capacity(3000);
b.put_slice(&[1u8; 100]);
b.advance(40);
let d = data_of(&l, &b);
println!("data={d:#x} ({}) len={} cap={}", decode(d), b.len(), b.capacity());
let (a0, s0) = allocs();
let empty = b.split_to(0);
let (a1, s1) = allocs();
println!("split_to(0): alloc+{} ({}B) {}", a1 - a0, s1 - s0, decode(data_of(&l, &b)));
drop(empty);
}
动脑筋答案
跑 3 次,输出相同:
data=0x509 (KIND_VEC bit1=0 repr=2 pos=40) len=60 cap=2960
split_to(0): alloc+1 (40B) KIND_ARC Shared 指针 (d & 0b111 = 0)
- 3000 右移 10 位是 2,有效位 2 位,
repr = 2;pos = 40。data = (40 << 5) | (2 << 2) | 1= 1280 + 8 + 1 = 1289 =0x509。capacity()是 3000 − 40 = 2960:前面 40 字节没丢,记在pos里,只是不算这个句柄的容量。 split_to(0)也分配了 40 字节的Shared。split_to不看at是不是 0,一律先shallow_clone,KIND_VEC就提升;advance_unchecked(0)虽然直接返回,提升已经发生了。这和Bytes::split_to不同:Bytes切出空段时不 clone(见 切片),BytesMut没有这个特判。
小结
BytesMut的data最低位 1 是KIND_VEC(独占一个Vec,data是一组位),0 是KIND_ARC(data指向Shared,靠Shared的偶数对齐天然为 0,1.6.0 起有编译期断言)。KIND_VEC时第 1 位不用(0.5.0 之前 kind 占两位留下的),第 2–4 位是原始容量的repr,第 5 位往上是pos。repr记容量去掉低 10 位之后的位宽,最多 7,还原成区间下界:小于 1024 记 0,最大还原 64 KiB。它只在from_vec时算一次,之后只搬运。pos是ptr相对Vec起点的偏移,drop、freeze、reserve、提升都靠它rebuild_vec。超过MAX_VEC_POS(64 位2^59 - 1,32 位 134217727)时promote_to_shared(1),wasm32 上实测分配 20 字节的Shared。promote_to_shared把repr抄进Shared、用pos还原完整的Vec;split 时计数从 2 开始,连split_to(0)也提升。回到KIND_VEC只有reserve另分配这一条路,repr抄回、pos归零。- 从
with_capacity(0)起步、靠reserve长大的缓冲(如 hyper 的读缓冲)repr是 0,共享之后的reserve不会被原始容量托底。