五种存储
主线位置:共享主线的分岔口。一个
Bytes从哪里构造出来,就决定了它挂哪张 vtable,之后的 clone、drop、is_unique、转回Vec/BytesMut都按那张表走。构建主线在freeze这一步汇入:BytesMut冻结时按自己的形态选表。
flowchart TB
st["静态切片"] --> vstatic["STATIC_VTABLE"]
boxed["Box u8 切片"] --> evenodd{"地址最低位"}
vecfull["Vec,len 等于 cap"] --> boxed
evenodd -->|0| veven["可提升 EVEN"]
evenodd -->|1| vodd["可提升 ODD"]
vecspare["Vec,len 小于 cap"] --> vshared["SHARED_VTABLE"]
veven -->|"第一次 clone"| vshared
vodd -->|"第一次 clone"| vshared
owner["from_owner"] --> vowned["Owned T 的表"]
fvec["freeze KIND_VEC"] --> vecfull
fvec --> vecspare
farc["freeze KIND_ARC"] --> vmut["BytesMut 共享表"]
上一页讲了 Bytes 的四个字段和 Vtable 的五个函数。这一页逐张看源码里的表。按存储方式算是五种,按表算是六处定义:
| 表 | 定义 | data 里放什么 | 从哪来 |
|---|---|---|---|
STATIC_VTABLE | const | 空指针 | from_static、new、From<&'static [u8]>、From<&'static str>、空的 Box<[u8]> |
PROMOTABLE_EVEN_VTABLE | static | 缓冲地址 | 1;提升后是 Shared 地址 | 非空 Box<[u8]>,地址是偶数 |
PROMOTABLE_ODD_VTABLE | static | 缓冲地址本身;提升后是 Shared 地址 | 非空 Box<[u8]>,地址是奇数 |
SHARED_VTABLE(bytes.rs) | static | Shared 地址 | len != cap 的 Vec;可提升存储 clone 出来的句柄 |
Owned::<T>::VTABLE | 关联 const,每个 T 一张 | Owned<T> 地址 | from_owner |
SHARED_VTABLE(bytes_mut.rs) | static | BytesMut 的 Shared 地址 | KIND_ARC 形态的 BytesMut::freeze |
From<Vec<u8>> 和 KIND_VEC 形态的 freeze 没有自己的表,它们落到前面几张上,下面分别讲。五个函数在每张表里的行为先列在一起,后面逐节展开:
| 表 | clone | is_unique | into_vec | into_mut | drop |
|---|---|---|---|---|---|
| 静态 | 返回同一切片的新句柄 | 总是 false | 拷贝 | 拷贝 | 什么都不做 |
| 可提升 | 第一次提升成 Shared,之后计数加一 | 未提升时 true,提升后看计数 | 未提升:数据挪到开头,还原 Vec | 未提升:还原 Vec,跳过前缀 | 未提升:释放缓冲;提升后减计数 |
Shared | 计数加一 | 计数是否为 1 | 唯一则复用缓冲,否则拷贝 | 唯一则复用缓冲,否则拷贝 | 减计数,到 0 释放 |
Owned<T> | 计数加一 | 总是 false | 拷贝后释放自己那份 | 同 into_vec | 减计数,到 0 drop owner |
BytesMut 的共享块 | 计数加一 | 计数是否为 1 | 唯一则复用缓冲,否则拷贝 | 唯一则原样变回 KIND_ARC 的 BytesMut | 减计数 |
「可提升」一行里提升之后的 into_vec / into_mut 和 Shared 一行相同,promotable_to_vec 看到 KIND_ARC 就直接调用 shared_to_vec_impl。
静态:STATIC_VTABLE
const STATIC_VTABLE: Vtable = Vtable {
clone: static_clone,
into_vec: static_to_vec,
into_mut: static_to_mut,
is_unique: static_is_unique,
drop: static_drop,
};
unsafe fn static_clone(_: &AtomicPtr<()>, ptr: *const u8, len: usize) -> Bytes {
let slice = slice::from_raw_parts(ptr, len);
Bytes::from_static(slice)
}
unsafe fn static_to_vec(_: *mut (), ptr: *const u8, len: usize) -> Vec<u8> {
let slice = slice::from_raw_parts(ptr, len);
slice.to_vec()
}
unsafe fn static_to_mut(_: *mut (), ptr: *const u8, len: usize) -> BytesMut {
let slice = slice::from_raw_parts(ptr, len);
BytesMut::from(slice)
}
fn static_is_unique(_: &AtomicPtr<()>) -> bool {
false
}
unsafe fn static_drop(_: *mut (), _: *const u8, _: usize) {
// nothing to drop for &'static [u8]
}
五个函数都不看 data(参数名是 _)。数据是 'static 的,没有谁需要被释放,clone 就是用同样的 ptr、len 再造一个 Bytes。static_clone 用的是当前句柄的 ptr 和 len,所以一个切过的静态 Bytes clone 出来还是那一段。
into_vec / into_mut 只能拷贝:静态内存不能交给 Vec 管。所以 is_unique 总是 false,这个方法的文档定义是「Into<BytesMut> would avoid cloning the underlying buffer」,静态数据永远做不到。
From<&'static str> 也是这条路,不分配:
impl From<&'static str> for Bytes {
fn from(slice: &'static str) -> Bytes {
Bytes::from_static(slice.as_bytes())
}
}
(源码)。注意参数类型是 &'static str:一个只活在栈上的 &str 不能 Bytes::from,要用 Bytes::copy_from_slice,那会分配并拷贝。
STATIC_VTABLE 是 const,别的表是 static,因为 from_static 是 const fnPR #311(「Make Bytes::from_static a const fn?」,0.5.0)提出的需求是:「so that Bytes and types built on Bytes can be used in consts and statics」。from_static 要在 const fn 里写 &STATIC_VTABLE,这个 PR 于是把 static STATIC_VTABLE 改成了 const STATIC_VTABLE,同时把最低 Rust 版本从 1.36 提到 1.39(提交说明:「Rust versions bumped to the minimum needed for const slice len」)。CHANGELOG 0.5.0:「Bytes::from_static is now a const fn (#311).」
代价是 STATIC_VTABLE 没有唯一的地址:const 在每个使用处展开成一份常量,&STATIC_VTABLE 在不同的地方可能指向不同的副本。bytes 自己从不拿 STATIC_VTABLE 的地址做比较;唯一一处比较 vtable 地址的 truncate 比的是两张 static 的可提升表。本页最后的实验里能看到同一张静态表出现了两个地址。
可提升:PROMOTABLE_EVEN 与 PROMOTABLE_ODD
构造:From<Box<[u8]>>
impl From<Box<[u8]>> for Bytes {
fn from(slice: Box<[u8]>) -> Bytes {
// Box<[u8]> doesn't contain a heap allocation for empty slices,
// so the pointer isn't aligned enough for the KIND_VEC stashing to
// work.
if slice.is_empty() {
return Bytes::new();
}
let len = slice.len();
let ptr = Box::into_raw(slice) as *mut u8;
if ptr as usize & 0x1 == 0 {
let data = ptr_map(ptr, |addr| addr | KIND_VEC);
Bytes {
ptr,
len,
data: AtomicPtr::new(data.cast()),
vtable: &PROMOTABLE_EVEN_VTABLE,
}
} else {
Bytes {
ptr,
len,
data: AtomicPtr::new(ptr.cast()),
vtable: &PROMOTABLE_ODD_VTABLE,
}
}
}
}
构造时不分配任何东西:Box 的缓冲直接交给 Bytes,data 里放的就是缓冲地址。没有引用计数,因为此刻只有这一个句柄。引用计数推迟到第一次 clone 时才分配,这就是「可提升」(promotable)的意思。
data 要同时表达两种形态:还没提升(data 指向缓冲)和已经提升(data 指向 Shared)。区分靠最低位,常量在 bytes.rs:
const KIND_ARC: usize = 0b0;
const KIND_VEC: usize = 0b1;
const KIND_MASK: usize = 0b1;
Shared 是 Box 分配的、对齐至少为 2 的结构体,地址最低位一定是 0,就是 KIND_ARC。源码用一个编译期断言守住这一点(源码):
const _: [(); 0 - mem::align_of::<Shared>() % 2] = []; // Assert that the alignment of `Shared` is divisible by 2.
如果对齐是奇数,0 - 1 在常量求值时溢出,编译失败。未提升时最低位是 1,就是 KIND_VEC。
为什么要两张表
缓冲是 u8 数组,对齐只有 1,分配器返回奇数地址完全合法。所以「把缓冲地址的最低位置 1」有两种情况:
- 偶数地址:最低位本来是 0,置 1 之后存进
data;要拿回缓冲地址时清掉最低位。这是PROMOTABLE_EVEN_VTABLE。 - 奇数地址:最低位本来就是 1,原样存进
data就已经是KIND_VEC;拿回缓冲地址时不能清掉最低位,否则就指向了缓冲前一个字节。这是PROMOTABLE_ODD_VTABLE。
两种情况下 data 都是「最低位为 1」,单看 data 分不出原来的地址是哪种。这一位信息记在「挂的是哪张表」上。两张表的区别只在怎样从 data 还原缓冲地址,对比两个 into_vec:
unsafe fn promotable_even_to_vec(shared: *mut (), ptr: *const u8, len: usize) -> Vec<u8> {
promotable_to_vec(shared, ptr, len, |shared| {
ptr_map(shared.cast(), |addr| addr & !KIND_MASK)
})
}
unsafe fn promotable_odd_to_vec(shared: *mut (), ptr: *const u8, len: usize) -> Vec<u8> {
promotable_to_vec(shared, ptr, len, |shared| shared.cast())
}
clone 和 drop 也一样:even 版本先 addr & !KIND_MASK,odd 版本直接用(promotable_even_clone、promotable_odd_clone)。is_unique 两张表共用 promotable_is_unique,它不需要还原地址。
还有一个前提:空的 Box<[u8]> 没有堆分配,地址是一个悬空的占位值(u8 的对齐是 1,这个值就是 1)。构造函数开头把空切片直接转成 Bytes::new(),注释说的就是这件事。
issue #343 的标题是「Bytes assumes that Vec<u8>'s allocation is more than 1 byte aligned」:「It uses the low bit of the pointer to distinguish between KIND_VEC and KIND_ARC.」当时只有一张 SharedVtable,它假设缓冲地址是偶数。
修复它的 PR #346(「Fix Bytes when Vec pointer's LSB is set」,0.5.4)把一张表拆成三张:「PromotableEvenVtable: The original SharedVtable, which will promote the Vec to Shared on the first clone, and is selected when the Vec's pointer has the LSB unset」;「PromotableOddVtable: ... This vtable differs in the masking used when reconstructing the Vec」;「SharedVtable: This no longer checks if its current kind is VEC or ARC, and is only created by the "promotable" vtables」。同时加了一个「"odd" global allocator that purposefully bumps all pointers with alignment of 1」的测试,就是下面那个文件。CHANGELOG 0.5.4:「Fix Bytes::from(Vec) when allocator gave Vec a pointer with LSB set.」
五个函数
clone:读 data,KIND_ARC 就是已经提升过,交给 shallow_clone_arc 把计数加一;KIND_VEC 就调用 shallow_clone_vec 分配一个计数为 2 的 Shared,用 CAS 把它装进 data。两种情况返回的新句柄都挂 SHARED_VTABLE;原句柄的 vtable 不变,只是 data 换成了 Shared 的地址。CAS 的竞争、内存序和失败时怎么回收,在 clone 与提升。
is_unique:KIND_VEC 直接返回 true。还没提升说明从来没被 clone 过,自己就是唯一的句柄。KIND_ARC 时看 Shared 的计数是不是 1。
into_vec:promotable_to_vec 在 KIND_VEC 时还原缓冲:
let buf = f(shared);
let cap = ptr.offset_from(buf) as usize + len;
// Copy back buffer
ptr::copy(ptr, buf, len);
Vec::from_raw_parts(buf, len, cap)
可提升存储不记容量。容量是算出来的:ptr - buf + len,即「当前可见段的末尾就是缓冲的末尾」。这个不变量成立,是因为 advance / split_to 只会把 ptr 往后挪,而会缩短末尾的 truncate 遇到这两张表时先用 split_off 提升(详见 切片)。ptr 前面可能被 advance 跳过一段,所以要先 ptr::copy 把数据挪回开头,再交给 Vec。
into_mut:promotable_to_mut 不挪数据,用 cap = off + len 还原一个满的 Vec,转成 BytesMut 后 advance_unchecked(off) 跳过前缀。BytesMut 能记住「前面切掉了多少」,Vec 不能,所以两者做法不同。
drop:KIND_ARC 时减计数;KIND_VEC 时调用 free_boxed_slice,按同样算出来的容量和对齐 1 直接 dealloc。
tests/test_bytes_odd_alloc.rs
用默认分配器很难跑到 PROMOTABLE_ODD_VTABLE:系统分配器给 Vec<u8> 的地址实际上都是偶数(本页最后的实验里,默认分配器下的 Vec 全部落到偶数表)。bytes 为此单独写了一个测试文件 tests/test_bytes_odd_alloc.rs,因为 #[global_allocator] 是整个测试二进制共享的,放进别的测试文件会影响所有用例。它的分配器只做一件事:
unsafe impl GlobalAlloc for Odd {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
if layout.align() == 1 && layout.size() > 0 {
// Allocate slightly bigger so that we can offset the pointer by 1
let size = layout.size() + 1;
let new_layout = match Layout::from_size_align(size, 1) {
Ok(layout) => layout,
Err(_err) => return ptr::null_mut(),
};
let ptr = System.alloc(new_layout);
if !ptr.is_null() {
ptr.offset(1)
} else {
ptr
}
} else {
System.alloc(layout)
}
}
// dealloc 对称地 offset(-1) 再释放
// ...
}
对齐为 1 的分配多要一个字节,返回地址加 1,所以 Vec<u8> 的缓冲地址一定是奇数;Shared 这类对齐为 8 的分配不受影响。文件开头是 #![cfg(not(miri))],注释说 Miri 不支持自定义分配器,而且「Miri is "odd" by default with 50% chance」。
8 个用例:
| 用例 | 覆盖的路径 |
|---|---|
sanity_check_odd_allocator | 先确认 vec![33u8; 1024] 的地址最低位确实是 1 |
test_bytes_from_vec_drop | 未提升时 drop(promotable_odd_drop 的 KIND_VEC 分支) |
test_bytes_clone_drop | clone 一次(提升)再 drop 两个句柄 |
test_bytes_into_vec | Vec::from 的五种情况:未提升;已提升且计数 1;已提升且计数 2;SHARED_VTABLE 的句柄计数 1;split_off 之后带偏移 |
test_bytesmut_from_bytes_vec | 未提升时 BytesMut::from |
test_bytesmut_from_bytes_arc_1 | 已提升、计数 1 时 BytesMut::from |
test_bytesmut_from_bytes_arc_2 | 已提升、计数 2,再对 SHARED_VTABLE 的另一半转换 |
test_bytesmut_from_bytes_arc_offset | split_off(20) 后两半分别转换 |
这些用例只比较内容,不检查挂的是哪张表。它们能抓住错误,是因为一旦用错了掩码,还原出来的地址差 1,dealloc 拿到的不是分配器给出的地址,测试进程会崩溃。
在 v1.12.1 源码的一份副本里运行(副本的 Cargo.toml 删掉了 dev-dependency serde_test:它只给 serde feature 下的 test_serde.rs 用,本机离线、缓存里也没有这个包;源码没有其他改动):
cargo test --test test_bytes_odd_alloc -- --test-threads=1
M2 Max、rustc 1.99.0,跑 3 次,输出相同:
running 8 tests
test sanity_check_odd_allocator ... ok
test test_bytes_clone_drop ... ok
test test_bytes_from_vec_drop ... ok
test test_bytes_into_vec ... ok
test test_bytesmut_from_bytes_arc_1 ... ok
test test_bytesmut_from_bytes_arc_2 ... ok
test test_bytesmut_from_bytes_arc_offset ... ok
test test_bytesmut_from_bytes_vec ... ok
test result: ok. 8 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
本页最后的实验借用了同一个分配器,直接看到 PROMOTABLE_ODD_VTABLE 上 data 等于缓冲地址本身。
From<Vec<u8>>:两条路
impl From<Vec<u8>> for Bytes {
fn from(vec: Vec<u8>) -> Bytes {
// Avoid an extra allocation if possible.
if vec.len() == vec.capacity() {
return Bytes::from(vec.into_boxed_slice());
}
let shared = Box::new(MaybeUninit::<Shared>::uninit());
let mut vec = ManuallyDrop::new(vec);
let ptr = vec.as_mut_ptr();
let len = vec.len();
let cap = vec.capacity();
let shared = Shared::init_to_raw(
shared,
Shared {
buf: ptr,
cap,
ref_cnt: AtomicUsize::new(1),
},
);
// ...
Bytes {
ptr,
len,
data: AtomicPtr::new(shared as _),
vtable: &SHARED_VTABLE,
}
}
}
条件就是 vec.len() == vec.capacity():
- 相等:
into_boxed_slice在长度等于容量时不需要重新分配,直接把缓冲交给Box<[u8]>,再走上一节的可提升路径。0 次分配。 - 不等:可提升存储不记容量(上一节,容量靠「可见段末尾就是缓冲末尾」算出来),带着空闲容量的
Vec不满足这个前提。这里不缩容,而是分配一个Shared,把buf和cap记在里面,计数从 1 开始,直接挂SHARED_VTABLE。1 次分配,24 字节。
Box::new(MaybeUninit::<Shared>::uninit()) 放在 ManuallyDrop::new(vec) 之前,是 1.12.1 唯一的修复(#837):Box::new 在配置成内存不足时 panic 的环境下可能展开,先分配、后接管 vec,展开时 vec 还能被正常释放。详见 怎么读这一卷。
From<String> 是 Bytes::from(s.into_bytes()),同样分两条路。
len == cap 这个判断是 1.4.0 为 freeze 加的issue #587 的标题是「BytesMut::freeze is not always zero-cost as documented」:「the operation can cause a re-allocation to occur because we call into_boxed_slice. This is unacceptable in some hot codepaths.」那时 From<Vec<u8>> 总是先 into_boxed_slice,而长度小于容量时这一步要缩容,重新分配并拷贝。
PR #592(「Avoid large reallocations when freezing BytesMut」,1.4.0)的做法是:「let's compare the length and capacity, and if they're the same, continue to handle this using into_boxed_slice. Otherwise, since we have a type of vtable which can handle a separate capacity, the shared vtable, let's turn our Vec into that kind of Bytes. While this does not avoid allocation altogether, it performs a fixed size allocation and avoids any need to memcpy.」就是上面代码的两条路。freeze 文档里的「The conversion is zero cost」至今没改,但 KIND_VEC 形态、len < cap 的 freeze 仍有这一次 24 字节的分配,下面的实验里能看到。
Vec::new() 的长度和容量都是 0,走第一条路,而空的 Box<[u8]> 又转成 Bytes::new(),所以最后是静态表。容量不为 0 的空 Vec 呢?
Vec 转成 Bytes,各分配几次?平台 M2 Max、rustc 1.99.0,bytes = "=1.12.1"。计数分配器只统计 Bytes::from 这一步:
use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering::Relaxed};
use bytes::Bytes;
struct Counting;
static ALLOCS: AtomicUsize = AtomicUsize::new(0);
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
ALLOCS.fetch_add(1, Relaxed);
unsafe { System.alloc(layout) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { System.dealloc(ptr, layout) }
}
}
#[global_allocator]
static A: Counting = Counting;
fn main() {
let a: Vec<u8> = Vec::new();
let b: Vec<u8> = Vec::with_capacity(10);
let before = ALLOCS.load(Relaxed);
let ba = Bytes::from(a);
let n_a = ALLOCS.load(Relaxed) - before;
let before = ALLOCS.load(Relaxed);
let bb = Bytes::from(b);
let n_b = ALLOCS.load(Relaxed) - before;
println!("Vec::new() -> allocs={n_a} len={} is_unique={}", ba.len(), ba.is_unique());
println!("Vec::with_capacity(10) -> allocs={n_b} len={} is_unique={}", bb.len(), bb.is_unique());
}
共享:SHARED_VTABLE
Shared 只记释放时要用的东西和一个计数:
struct Shared {
// Holds arguments to dealloc upon Drop, but otherwise doesn't use them
buf: *mut u8,
cap: usize,
ref_cnt: AtomicUsize,
}
impl Drop for Shared {
fn drop(&mut self) {
unsafe { dealloc(self.buf, Layout::from_size_align(self.cap, 1).unwrap()) }
}
}
它不是 Arc<Vec<u8>>:没有弱引用计数,缓冲和计数也分在两块分配里(计数块是后加的,缓冲还是原来 Vec 的那块)。各个函数:
- clone(
shared_clone):Relaxed读出data,交给shallow_clone_arc:fetch_add(1, Relaxed),旧值超过usize::MAX >> 1就abort,和标准库Arc防计数溢出的做法一样。新句柄的ptr、len照抄。 - is_unique(
shared_is_unique):计数是否为 1。 - into_vec(
shared_to_vec_impl):用compare_exchange(1, 0, AcqRel, Relaxed)判断唯一。成功就释放Shared这个计数块(不跑它的Drop,否则缓冲也被释放了),把数据ptr::copy到缓冲开头,用记下的cap还原Vec;失败就拷贝出一个新Vec,再减掉自己的计数。 - into_mut(
shared_to_mut_impl):计数Acquire读出来是 1 就复用缓冲,还原Vec后advance_unchecked(off);否则拷贝。 - drop(
shared_drop):release_shared,fetch_sub(1, Release),减到 0 时一次Acquire读再释放,就是Arc的 drop 步骤。注释说 TSAN 不支持原子 fence,所以用一次load(Acquire)代替fence(Acquire)。
这张表的 clone 不再检查 KIND_VEC / KIND_ARC:挂这张表的 Bytes,data 一定是 Shared。为什么 into_vec 用 CAS 而 into_mut 只用一次读,以及几处内存序的配对,在 clone 与提升。
拥有者:Owned<T>
from_owner 把任意 T: AsRef<[u8]> + Send + 'static 包进一个带计数的盒子,典型用法是内存映射文件:
#[repr(C)]
struct Owned<T> {
ref_cnt: AtomicUsize,
owner: T,
}
impl<T> Owned<T> {
const VTABLE: Vtable = Vtable {
clone: owned_clone::<T>,
into_vec: owned_to_vec::<T>,
into_mut: owned_to_mut::<T>,
is_unique: owned_is_unique,
drop: owned_drop::<T>,
};
}
(Owned)有两处值得看:
#[repr(C)]让ref_cnt一定在偏移 0。owned_clone和owned_drop_impl都是把data直接cast::<AtomicUsize>()来读写计数,只有 drop 最后一步才需要知道T,用Box::<Owned<T>>::from_raw释放。owned_is_unique根本不读data,所以它不是泛型函数。- 表是关联
const,每个T单态化出一张。drop要调用T的析构,只能按T生成。1.11.0 之前,析构函数指针和 owner 一起存在堆上的盒子里(PR #801:「Currently we store the drop function pointer together with the data object when usingfrom_owner. This is not necessary, as the drop function can be stored in the vtable.」)。
行为上:clone 是计数加一,drop 是计数减一、到 0 时 drop owner。is_unique 总是 false,into_vec / into_mut 总是拷贝,然后减掉自己的计数。
问: 一个 from_owner 出来、从没 clone 过的 Bytes,明明只有它一个句柄,is_unique 为什么也返回 false?
答: is_unique 问的不是「有没有别的句柄」,文档的定义是「this is the only reference to the data and Into<BytesMut> would avoid cloning the underlying buffer」。BytesMut 要把缓冲当成一个 Vec<u8> 来管理(扩容、释放都按 Vec 的布局),而 owner 的内存可能是内存映射、可能是别的分配器给的,不能交给 Vec。所以转成 BytesMut 一定要拷贝,try_into_mut 一定返回 Err。from_owner 的文档也写明「converting [Bytes] constructed from an owner into a [BytesMut] will always create a deep copy」。静态表返回 false 是同一个原因。
BytesMut::freeze 落到哪张表
pub fn freeze(self) -> Bytes {
let bytes = ManuallyDrop::new(self);
if bytes.kind() == KIND_VEC {
// Just re-use `Bytes` internal Vec vtable
unsafe {
let off = bytes.get_vec_pos();
let vec = rebuild_vec(bytes.ptr.as_ptr(), bytes.len, bytes.cap, off);
let mut b: Bytes = vec.into();
b.advance(off);
b
}
} else {
debug_assert_eq!(bytes.kind(), KIND_ARC);
let ptr = bytes.ptr.as_ptr();
let len = bytes.len;
let data = AtomicPtr::new(bytes.data.cast());
unsafe { Bytes::with_vtable(ptr, len, data, &SHARED_VTABLE) }
}
}
BytesMut 有两种形态(编码见 data 字段的编码),freeze 分开处理:
KIND_VEC:BytesMut还独占一个Vec,前面可能被切掉了off字节。rebuild_vec把切掉的部分加回去,还原出原来的Vec(长度off + len,容量off + cap),然后走From<Vec<u8>>,最后advance(off)跳过前缀。所以这里落到哪张表取决于len == cap:写满了是可提升存储,没写满是bytes.rs的SHARED_VTABLE。注释里的「Bytes internal Vec vtable」指的就是这两条路。KIND_ARC:BytesMut已经和别的句柄共享一个bytes_mut.rs里的Shared(它装的是整个Vec、原始容量和计数,40 字节,和bytes.rs的 24 字节Shared不是一个类型)。freeze不分配,把这个指针原样放进data,挂bytes_mut.rs自己的SHARED_VTABLE。
后一张表的函数(shared_v_clone 到 shared_v_drop)和 bytes.rs 的共享表一一对应,差别在 shared_v_to_mut:唯一时它不还原 Vec,直接造一个 data 仍指向这个 Shared 的 BytesMut,也就是回到 KIND_ARC 形态,原始容量等信息都保留着。
实验:每种构造落在哪张表
公开 API 能看到分配次数、is_unique 和 try_into_mut 的结果,但看不到挂的是哪张表。下面的程序额外把 Bytes 当成 4 个 usize 读出来,拿到 vtable 和 data 两个字。这依赖私有布局,只用于观察:字段顺序由编译器决定,程序先用一个 from_static 定位(ptr、len 和公开 API 对上,剩下两个字里为 0 的是 data),之后每次读都再校验一次 ptr、len。vtable 地址按第一次出现的顺序编号为 A、B、C……。分配器沿用上一节测试文件的做法,ODD 打开时把对齐为 1 的分配挪到奇数地址。
bytes = "=1.12.1"
use std::alloc::{GlobalAlloc, Layout, System};
use std::mem::size_of;
use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed};
use bytes::{BufMut, Bytes, BytesMut};
// ---- 计数分配器;ODD 打开时,对齐为 1 的分配返回奇数地址(同 tests/test_bytes_odd_alloc.rs)----
struct Counting;
static ALLOCS: AtomicUsize = AtomicUsize::new(0);
static ODD: AtomicBool = AtomicBool::new(false);
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
ALLOCS.fetch_add(1, Relaxed);
if ODD.load(Relaxed) && layout.align() == 1 && layout.size() > 0 {
let l = Layout::from_size_align(layout.size() + 1, 1).unwrap();
let p = unsafe { System.alloc(l) };
return if p.is_null() { p } else { unsafe { p.add(1) } };
}
unsafe { System.alloc(layout) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
// 奇数地址一定来自上面的分支(System 给的地址至少按 8 对齐)
if layout.align() == 1 && layout.size() > 0 && (ptr as usize) & 1 == 1 {
let l = Layout::from_size_align(layout.size() + 1, 1).unwrap();
unsafe { System.dealloc(ptr.sub(1), l) }
} else {
unsafe { System.dealloc(ptr, layout) }
}
}
}
#[global_allocator]
static A: Counting = Counting;
// ---- 只用于观察:把 Bytes 当成 4 个 usize 读(依赖私有布局)----
// 字段顺序由编译器决定,所以先用 from_static 定位:ptr、len 用公开 API 对上,
// 剩下两个字里 from_static 为 0 的是 data,另一个是 vtable。
struct Slots {
ptr: usize,
len: usize,
data: usize,
vtable: usize,
}
fn words(b: &Bytes) -> [usize; 4] {
assert_eq!(size_of::<[usize; 4]>(), size_of::<Bytes>());
unsafe { *(b as *const Bytes as *const [usize; 4]) }
}
fn locate() -> Slots {
let b = Bytes::from_static(b"locate me");
let w = words(&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 data = (0..4).find(|&i| i != ptr && i != len && w[i] == 0).unwrap();
let vtable = (0..4).find(|&i| i != ptr && i != len && i != data).unwrap();
println!("layout: ptr=word{ptr} len=word{len} data=word{data} vtable=word{vtable}");
Slots { ptr, len, data, vtable }
}
static SLOTS: Mutex<Option<Slots>> = Mutex::new(None);
static SEEN: Mutex<Vec<usize>> = Mutex::new(Vec::new());
fn peek(b: &Bytes) -> (char, usize) {
let w = words(b);
let guard = SLOTS.lock().unwrap();
let s = guard.as_ref().unwrap();
// 每次都校验:ptr/len 必须与公开 API 一致
assert_eq!(w[s.ptr], b.as_ptr() as usize);
assert_eq!(w[s.len], b.len());
let addr = w[s.vtable];
let data = w[s.data];
let mut seen = SEEN.lock().unwrap();
let idx = match seen.iter().position(|&a| a == addr) {
Some(i) => i,
None => {
seen.push(addr);
seen.len() - 1
}
};
(char::from(b'A' + idx as u8), data)
}
fn describe_data(b: &Bytes, data: usize) -> String {
if data == 0 {
"null".into()
} else if data == b.as_ptr() as usize {
"=ptr".into()
} else if data == (b.as_ptr() as usize | 1) {
"ptr|1".into()
} else {
format!("other,lsb={}", data & 1)
}
}
fn allocs<R>(f: impl FnOnce() -> R) -> (R, usize) {
let before = ALLOCS.load(Relaxed);
let r = f();
(r, ALLOCS.load(Relaxed) - before)
}
fn row<T>(name: &str, prep: impl Fn() -> T, conv: impl Fn(T) -> Bytes) {
// 只把「转成 Bytes」这一步算进 new_allocs,准备输入的分配不算
let input = prep();
let (b, n) = allocs(|| conv(input));
let (vt, data) = peek(&b);
let unique = b.is_unique();
let (c, n_clone) = allocs(|| b.clone());
let (cvt, _) = peek(&c);
let (after, data_after) = peek(&b);
drop(c);
let ok = conv(prep()).try_into_mut().is_ok();
println!(
"{name:<26} vt={vt} data={:<5} new={n} unique={unique:<5} clone={n_clone} clone_vt={cvt} after: vt={after} data={:<5} try_into_mut={}",
describe_data(&b, data),
describe_data(&b, data_after),
if ok { "Ok" } else { "Err" },
);
}
fn main() {
*SLOTS.lock().unwrap() = Some(locate());
row("from_static", || (), |()| Bytes::from_static(b"hello"));
row("new()", || (), |()| Bytes::new());
row("From<&'static str>", || "hello", Bytes::from);
row("From<Vec> len==cap", || vec![7u8; 64], Bytes::from);
row(
"From<Vec> len<cap",
|| {
let mut v = Vec::with_capacity(64);
v.extend_from_slice(b"hello");
v
},
Bytes::from,
);
row("From<Vec> empty cap=0", Vec::new, Bytes::from);
row("From<Box<[u8]>>", || vec![7u8; 64].into_boxed_slice(), Bytes::from);
row("from_owner(Vec)", || vec![7u8; 64], Bytes::from_owner);
row(
"freeze KIND_VEC len<cap",
|| {
let mut m = BytesMut::with_capacity(64);
m.put_slice(b"hello");
m
},
BytesMut::freeze,
);
row("freeze KIND_VEC len==cap", || BytesMut::from(&[7u8; 64][..]), BytesMut::freeze);
row(
"freeze KIND_ARC",
|| {
let mut m = BytesMut::with_capacity(64);
m.put_slice(b"hello world");
drop(m.split_to(6));
m
},
BytesMut::freeze,
);
ODD.store(true, Relaxed);
row("ODD From<Vec> len==cap", || vec![7u8; 64], Bytes::from);
ODD.store(false, Relaxed);
// A(在本 crate 里内联出来的 from_static)和 D(bytes 内部 Bytes::new)的 5 个函数指针
let seen = SEEN.lock().unwrap().clone();
let body = |a: usize| unsafe { *(a as *const [usize; 5]) };
let same = body(seen[0]).iter().zip(body(seen[3]).iter()).filter(|(x, y)| x == y).count();
println!("A 与 D 的 5 个函数指针里相同的有 {same} 个");
}
M2 Max、rustc 1.99.0,--release,跑 3 次,输出相同(vtable 的编号是按出现顺序给的,不受地址随机化影响):
layout: ptr=word1 len=word2 data=word3 vtable=word0
from_static vt=A data=null new=0 unique=false clone=0 clone_vt=A after: vt=A data=null try_into_mut=Err
new() vt=A data=null new=0 unique=false clone=0 clone_vt=A after: vt=A data=null try_into_mut=Err
From<&'static str> vt=A data=null new=0 unique=false clone=0 clone_vt=A after: vt=A data=null try_into_mut=Err
From<Vec> len==cap vt=B data=ptr|1 new=0 unique=true clone=1 clone_vt=C after: vt=B data=other,lsb=0 try_into_mut=Ok
From<Vec> len<cap vt=C data=other,lsb=0 new=1 unique=true clone=0 clone_vt=C after: vt=C data=other,lsb=0 try_into_mut=Ok
From<Vec> empty cap=0 vt=D data=null new=0 unique=false clone=0 clone_vt=D after: vt=D data=null try_into_mut=Err
From<Box<[u8]>> vt=B data=ptr|1 new=0 unique=true clone=1 clone_vt=C after: vt=B data=other,lsb=0 try_into_mut=Ok
from_owner(Vec) vt=E data=other,lsb=0 new=1 unique=false clone=0 clone_vt=E after: vt=E data=other,lsb=0 try_into_mut=Err
freeze KIND_VEC len<cap vt=C data=other,lsb=0 new=1 unique=true clone=0 clone_vt=C after: vt=C data=other,lsb=0 try_into_mut=Ok
freeze KIND_VEC len==cap vt=B data=ptr|1 new=0 unique=true clone=1 clone_vt=C after: vt=B data=other,lsb=0 try_into_mut=Ok
freeze KIND_ARC vt=F data=other,lsb=0 new=0 unique=true clone=0 clone_vt=F after: vt=F data=other,lsb=0 try_into_mut=Ok
ODD From<Vec> len==cap vt=G data==ptr new=0 unique=true clone=1 clone_vt=C after: vt=G data=other,lsb=0 try_into_mut=Ok
A 与 D 的 5 个函数指针里相同的有 2 个
(data==ptr 是 data= 后面接 =ptr,表示 data 等于缓冲地址本身。)
逐个对上源码:
| 编号 | 是哪张表 | 依据 |
|---|---|---|
| A | STATIC_VTABLE | data 为空,0 次分配,is_unique 为 false,try_into_mut 失败 |
| B | PROMOTABLE_EVEN_VTABLE | data 是缓冲地址 | 1;第一次 clone 分配 1 次(Shared),之后自己的 data 变成最低位为 0 的 Shared 地址,表不变 |
| C | bytes.rs 的 SHARED_VTABLE | B、G 的 clone 都挂它;len < cap 的 Vec 直接得到它,构造时分配 1 次,clone 0 次 |
| D | 还是 STATIC_VTABLE | 行为和 A 完全一样;地址不同,见下 |
| E | Owned::<Vec<u8>>::VTABLE | 构造分配 1 次(Owned 盒子),is_unique 为 false,try_into_mut 失败 |
| F | bytes_mut.rs 的 SHARED_VTABLE | KIND_ARC 的 freeze,0 次分配,和 C 不是同一张表 |
| G | PROMOTABLE_ODD_VTABLE | 奇数地址时 data 就是缓冲地址本身;clone 出来同样挂 C |
几点观察:
freeze的三行分别落到 C、B、F:KIND_VEC时完全复用From<Vec<u8>>的两条路,len < cap那一行有 1 次分配,就是 #592 留下的那个 24 字节Shared;只有KIND_ARC才用bytes_mut.rs的表。- 可提升存储 clone 之后,原句柄
after一栏的表还是 B(或 G),data已经变成最低位为 0 的地址。表没换,data换了,后续调用靠最低位判断形态。 - D 和 A 都是静态表,却是两个地址,而且 5 个函数指针里只有 2 个相同。A 来自本程序里内联展开的
Bytes::from_static,D 来自 bytes 内部From<Box<[u8]>>里的Bytes::new():STATIC_VTABLE是const,每处使用各有一份;表里的小函数也被各自生成了副本。这就是上面冷知识说的「没有唯一地址」。 freeze KIND_ARC这一行的is_unique为true,是因为程序把split_to切出去的那一半立即 drop 了,计数回到 1。
本机的 nightly 没装 Miri(cargo +nightly miri 报 'cargo-miri' is not installed),这个程序没有在 Miri 下跑过。
动脑筋答案
Vec::new() -> allocs=0 len=0 is_unique=false
Vec::with_capacity(10) -> allocs=1 len=0 is_unique=true
跑 3 次,输出相同。
Vec::new():长度 0 等于容量 0,走into_boxed_slice,空的Box<[u8]>又被转成Bytes::new(),落在静态表上,0 次分配,is_unique为false。Vec::with_capacity(10):长度 0 不等于容量 10,走第二条路,分配一个Shared记下这 10 字节的缓冲,1 次分配,挂SHARED_VTABLE,计数为 1,is_unique为true。一个空的Bytes就这样占着 10 字节的缓冲和 24 字节的计数块,直到它被 drop。
小结
- 五种存储、六处表定义:静态(
const)、可提升奇偶两张、bytes.rs的Shared、每个T一张的Owned<T>、bytes_mut.rs的共享表。表决定 clone / drop /is_unique/into_*怎么做,data的含义由表解释。 - 静态表什么都不分配、不释放;
From<&'static str>和Bytes::new()都走这里。is_unique永远false,转回Vec/BytesMut只能拷贝。 - 可提升存储构造时 0 次分配,第一次 clone 才把
data换成Shared(clone 与提升)。它不记容量,容量靠「可见段末尾就是缓冲末尾」算出。data的最低位为 1 表示未提升;缓冲地址本来是奇数时原样存放,所以要靠两张表记住「还原时要不要清最低位」(0.5.4,#346)。 From<Vec<u8>>:len == cap走Box<[u8]>,0 次分配;否则分配 24 字节的Shared直接挂共享表(1.4.0,#592)。空Vec落到静态表。Owned<T>用#[repr(C)]把计数放在偏移 0,clone 和计数操作不需要知道T;它的is_unique永远false。freeze:KIND_VEC复用From<Vec<u8>>的两条路再跳过前缀,KIND_ARC不分配,挂bytes_mut.rs的共享表。