split 与 freeze
主线位置:构建主线的中段,也是两条主线的交接处。
split_to/split_off/split把一个独占的BytesMut分成几个互不重叠的可写句柄,第一次分时data从KIND_VEC变成KIND_ARC;freeze把其中一个交给共享主线变成Bytes;From<Bytes> for BytesMut和unsplit是往回走的两条路。
stateDiagram-v2 state "KIND_VEC 独占 Vec" as KindVec state "KIND_ARC 共享 Shared" as KindArc state "Bytes" as Frozen [*] --> KindVec: with_capacity KindVec --> KindArc: 第一次 split KindArc --> KindArc: 再 split 或 unsplit KindVec --> Frozen: freeze 走 From Vec KindArc --> Frozen: freeze 换 vtable Frozen --> KindVec: 转回,拷贝或复用 Vec Frozen --> KindArc: 转回,Shared 唯一
BytesMut 的四个字段和 data 的两种形态怎么编码,在 data 字段的编码。这一页只用到一点:data 最低位是 1 时是 KIND_VEC,BytesMut 还独占一个 Vec,其余位记着前面被跳过了多少字节;最低位是 0 时是 KIND_ARC,data 就是一个 Shared 的地址,几个句柄共用它。一块缓冲的一生 已经照 hyper 的读路径把 split_to 和 freeze 跑过一遍,这里把每个函数拆开看,再补上反方向的 unsplit、try_unsplit 和 From<Bytes>。
三个 split:先 shallow_clone,再各改各的
split_off 和 split_to 的结构一样:检查边界,shallow_clone 出一个和自己逐位相同的句柄,再把两个句柄的 ptr / len / cap 改成不重叠的两段。
pub fn split_off(&mut self, at: usize) -> BytesMut {
assert!(
at <= self.capacity(),
"split_off out of bounds: {:?} <= {:?}",
at,
self.capacity(),
);
unsafe {
// ...
let mut other = self.shallow_clone();
// SAFETY: We've checked that `at` <= `self.capacity()` above.
other.advance_unchecked(at);
self.cap = at;
self.len = cmp::min(self.len, at);
other
}
}
pub fn split_to(&mut self, at: usize) -> BytesMut {
assert!(
at <= self.len(),
"split_to out of bounds: {:?} <= {:?}",
at,
self.len(),
);
unsafe {
// ...
let mut other = self.shallow_clone();
// ...
self.advance_unchecked(at);
other.cap = at;
other.len = at;
other
}
}
split 就是 self.split_to(self.len()):把已写的部分全部切走,self 留下空的长度和剩余的容量。
两个边界检查不一样,是因为两边切的东西不一样:
| 方法 | 返回值 | self 剩下 | 检查 |
|---|---|---|---|
split_off(at) | [at, cap),可以全是没写过的容量 | [0, at) | at <= capacity() |
split_to(at) | [0, at),必须是已写的数据 | [at, cap) | at <= len() |
split_off 切的是容量,at 可以落在还没写的区域,这时 self.len 被 cmp::min 截到 at,other 的长度由 advance_unchecked 里的 saturating_sub 算成 0。split_to 交出去的那段要能当作数据读,所以只能切已写的部分。两种情况下前面那段的 cap 都被截到 at:前一个句柄往后写,只能写到 at,碰不到后一个句柄的字节,容量不够时它走 reserve 另找地方(见 reserve)。两个可写句柄共用一块内存而不需要锁,靠的就是这一步。
shallow_clone:第一次分配 Shared,以后只加计数
unsafe fn shallow_clone(&mut self) -> BytesMut {
if self.kind() == KIND_ARC {
increment_shared(self.data);
ptr::read(self)
} else {
self.promote_to_shared(/*ref_count = */ 2);
ptr::read(self)
}
}
ptr::read(self) 是逐位复制,复制出来的句柄和 self 指向同一段、同一个 Shared,所以它是 unsafe 的:调用方必须马上把两个句柄改成不重叠。KIND_ARC 时 increment_shared 把计数 Relaxed 加一。KIND_VEC 时先调用 promote_to_shared(2),它的核心是:
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),
},
);
// ...
self.data = shared;
Shared 装着用 rebuild_vec 拼回来的整个 Vec(把前面跳过的 off 字节加回去)、原始容量的编码和计数,64 位上是 24 + 8 + 8 = 40 字节。计数直接从 2 开始,因为 shallow_clone 返回之后就有两个句柄。self.data 换成 Shared 的地址,最低位变成 0;随后 ptr::read 复制出来的句柄也带着这个地址。先 Box::new 一块未初始化的内存、再 rebuild_vec,是 1.12.1 唯一的修复(#837),在 怎么读这一卷 里讲过。original_capacity_repr 和 off 怎么从 data 里取出来,见 data 字段的编码。
提升之后,split 再改 ptr 用的是 advance_unchecked。它在 KIND_VEC 时要把偏移记进 data,偏移超过 MAX_VEC_POS 时也会 promote_to_shared(1);在 KIND_ARC 时只改 ptr、len、cap。split 走到这里时两个句柄都已经是 KIND_ARC,所以只有后一种情况。
实验:切三刀,再按原样并回去
common.rs 是一个计数分配器(包住 System,只数次数和字节)加两个打印辅助:count 跑一段闭包并返回这段时间的分配增量,at 打印指针相对缓冲起点的偏移,不在这块缓冲里就打印 elsewhere。
bytes = "=1.12.1"
use bytes::{BufMut, BytesMut};
#[path = "../common.rs"]
mod common;
use common::*;
fn show(name: &str, b: &BytesMut, base: *const u8) -> String {
format!("{name}: ptr={} len={} cap={}", at(b.as_ptr(), base, 64), b.len(), b.capacity())
}
fn main() {
let mut buf = BytesMut::with_capacity(64);
buf.put_slice(b"aaabbbcccddd");
let base = buf.as_ptr();
let (mut tail, d) = count(|| buf.split_off(6));
println!("{:<18} {d} {} | {}", "split_off(6)", show("buf", &buf, base), show("tail", &tail, base));
let (mid, d) = count(|| tail.split_to(3));
println!("{:<18} {d} {} | {}", "tail.split_to(3)", show("mid", &mid, base), show("tail", &tail, base));
let (rest, d) = count(|| tail.split());
println!("{:<18} {d} {} | {}", "tail.split()", show("rest", &rest, base), show("tail", &tail, base));
let (_, d) = count(|| buf.unsplit(mid));
println!("{:<18} {d} {}", "buf.unsplit(mid)", show("buf", &buf, base));
let (_, d) = count(|| buf.unsplit(rest));
println!("{:<18} {d} {}", "buf.unsplit(rest)", show("buf", &buf, base));
let (_, d) = count(|| buf.unsplit(tail));
println!("{:<18} {d} {}", "buf.unsplit(tail)", show("buf", &buf, base));
println!("{:<18} {:?}", "content", std::str::from_utf8(&buf).unwrap());
let (_, d) = count(|| drop(buf));
println!("{:<18} {d}", "drop(buf)");
// 空的也会提升
let mut empty = BytesMut::with_capacity(64);
let (e, d) = count(|| empty.split());
println!("{:<18} {d} e.len={} e.cap={} empty.cap={}", "empty.split()", e.len(), e.capacity(), empty.capacity());
let mut zero = BytesMut::new();
let (z, d) = count(|| zero.split());
println!("{:<18} {d} z.cap={}", "new().split()", z.capacity());
}
M2 Max、rustc 1.99.0,--release,跑了 3 次,输出相同:
split_off(6) alloc+1 (40B) free+0 buf: ptr=base+0 len=6 cap=6 | tail: ptr=base+6 len=6 cap=58
tail.split_to(3) alloc+0 (0B) free+0 mid: ptr=base+6 len=3 cap=3 | tail: ptr=base+9 len=3 cap=55
tail.split() alloc+0 (0B) free+0 rest: ptr=base+9 len=3 cap=3 | tail: ptr=base+12 len=0 cap=52
buf.unsplit(mid) alloc+0 (0B) free+0 buf: ptr=base+0 len=9 cap=9
buf.unsplit(rest) alloc+0 (0B) free+0 buf: ptr=base+0 len=12 cap=12
buf.unsplit(tail) alloc+0 (0B) free+0 buf: ptr=base+0 len=12 cap=64
content "aaabbbcccddd"
drop(buf) alloc+0 (0B) free+2
- 只有第一刀分配,正好 40 字节,就是
Shared。后两刀只加计数。 - 所有指针都是
base+N:每个句柄的ptr加cap正好是下一个句柄的ptr(0+6、6+3、9+3),四段拼起来是完整的 64 字节,没有一个字节被搬动。 - 按原来的顺序
unsplit回去,三次都是 0 分配,cap一路长回 64,ptr还是base+0。最后一个tail长度为 0、容量 52,它并回来的是容量。 drop(buf)释放两次:计数从 1 减到 0,先释放Shared里的Vec(那 64 字节),再释放Shared本身。
最后两行是空 BytesMut 的 split:
empty.split() alloc+1 (40B) free+0 e.len=0 e.cap=0 empty.cap=64
new().split() alloc+1 (40B) free+0 z.cap=0
切出来的东西长度和容量都是 0,照样分配了 40 字节。BytesMut 的 split 没有空结果的捷径,shallow_clone 无条件执行,连容量为 0 的 BytesMut::new() 也会提升。Bytes 不一样:Bytes::split_to 在 at == len 或 at == 0 时直接返回空句柄、不碰计数(split_to,详见 切片)。循环里对可能为空的缓冲反复 split(),第一次会多付一次分配;之后已经是 KIND_ARC,只是加减计数。
越界时的 panic 信息
两个 assert! 都没有 #[track_caller],panic 位置落在 bytes_mut.rs 里,不是调用方。用和 切片 一样的 panic hook 打出原文(len 11、cap 64):
use std::panic;
use bytes::{BufMut, BytesMut};
fn main() {
panic::set_hook(Box::new(|info| {
let msg = info.payload().downcast_ref::<String>().cloned()
.or_else(|| info.payload().downcast_ref::<&str>().map(|s| s.to_string()))
.unwrap_or_default();
let loc = info.location().unwrap();
let file = loc.file().rsplit("bytes-1.12.1/").next().unwrap();
println!(" panicked at {file}:{}: {msg}", loc.line());
}));
let make = || {
let mut b = BytesMut::with_capacity(64);
b.put_slice(b"hello world"); // len 11, cap 64
b
};
let cases: Vec<(&str, Box<dyn Fn()>)> = vec![
("split_to(20)", Box::new(move || drop(make().split_to(20)))),
("split_off(65)", Box::new(move || drop(make().split_off(65)))),
];
for (name, f) in cases {
println!("{name}");
let _ = panic::catch_unwind(panic::AssertUnwindSafe(f));
}
let mut b = make();
let t = b.split_off(20);
println!("split_off(20): no panic, self len={} cap={}, other len={} cap={}",
b.len(), b.capacity(), t.len(), t.capacity());
}
跑了 3 次,输出相同:
split_to(20)
panicked at src/bytes_mut.rs:408: split_to out of bounds: 20 <= 11
split_off(65)
panicked at src/bytes_mut.rs:329: split_off out of bounds: 65 <= 64
split_off(20): no panic, self len=11 cap=20, other len=0 cap=44
信息的格式是「at <= 上限」,读作「要求 at 不大于这个数」,不是在说这个不等式成立。split_to 的上限是 len(11),split_off 的上限是 capacity(64)。split_off(20) 超过了长度但没超过容量,不 panic:self 保留全部 11 字节、容量缩到 20,other 拿到 44 字节的空容量。Bytes::split_off 没有容量的概念,同样的 20 在那里会 panic(见 切片 的输出)。
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) }
}
}
ManuallyDrop 防止 BytesMut::drop 在函数结束时释放缓冲,所有权整个交给新的 Bytes。
KIND_VEC(从没 split 过):rebuild_vec把前面跳过的off字节加回去拼出原来的Vec,交给From<Vec<u8>>,再advance(off)跳过前缀。Vec写满了(len == cap)落到可提升存储,不分配;没写满落到bytes.rs的SHARED_VTABLE,分配一个 24 字节的Shared。两条路和分配次数在 五种存储 和 一块缓冲的一生 都量过,后一页还讲了 1.4.0 之前这一步会把整块缓冲重新分配一遍(#592)。KIND_ARC(split 过):Shared已经有了,data原样搬进Bytes,挂bytes_mut.rs自己的SHARED_VTABLE。不分配,计数不变:原来的BytesMut句柄变成了Bytes句柄,句柄总数没变。
所以 hyper 的 read_buf.split_to(n).freeze() 这种写法,split_to 已经把缓冲提升成 KIND_ARC,紧接着的 freeze 只是换一张表。From<BytesMut> for Bytes 就是调用 freeze;BytesMut 的 Buf::copy_to_bytes 是 self.split_to(len).freeze(),这也是它不拷贝的原因(Buf 那一侧见 Buf)。
clone 与 From<Bytes>:拷贝还是复用
clone 是深拷贝
impl Clone for BytesMut {
fn clone(&self) -> BytesMut {
BytesMut::from(&self[..])
}
}
From<&[u8]> 是 BytesMut::from_vec(src.to_vec()):新分配一块,长度等于原来的 len,把字节拷过去。原来的空闲容量不跟过去,KIND_VEC / KIND_ARC 也不跟过去,clone 出来的总是一个新的 KIND_VEC。
问: split 能让两个 BytesMut 共用一块内存,为什么 clone 不能也只加个计数?
答: split 之所以安全,是因为它同时把两个句柄改成了不重叠的两段,每个字节只有一个句柄能写。clone 要的是两个句柄看到同样的字节,如果共用内存,一个句柄 buf[0] = x 另一个就看得到,&mut [u8] 的独占保证就破了。要共享只读的副本,先 freeze 再 clone Bytes,那才是只加计数。
From<Bytes> for BytesMut
From<Bytes> for BytesMut 只做一次分派:
fn from(bytes: Bytes) -> Self {
let mut bytes = ManuallyDrop::new(bytes);
let data = bytes.data_mut();
unsafe { (bytes.vtable.into_mut)(data, bytes.ptr, bytes.len) }
}
每张表的 into_mut 在 五种存储 列过,bytes.rs 那几张表的唯一路径在 clone 与提升 的 try_into_mut 实验里量过:唯一时原地重建 Vec,不唯一时拷贝。和本页相关的是 freeze 出来的 KIND_ARC 句柄走的 shared_v_to_mut:
if (*shared).is_unique() {
let shared = &mut *shared;
// The capacity is always the original capacity of the buffer
// minus the offset from the start of the buffer
let v = &mut shared.vec;
let v_capacity = v.capacity();
let v_ptr = v.as_mut_ptr();
let offset = ptr.offset_from(v_ptr) as usize;
let cap = v_capacity - offset;
let ptr = vptr(ptr as *mut u8);
BytesMut {
ptr,
len,
cap,
data: shared,
}
} else {
let v = slice::from_raw_parts(ptr, len).to_vec();
release_shared(shared);
BytesMut::from_vec(v)
}
唯一时它不拆 Shared,直接造一个 data 仍指向它的 BytesMut,也就是回到 KIND_ARC;容量是「Vec 的总容量减去自己的起点偏移」,一直算到整块缓冲的末尾。不唯一时拷贝 len 字节,再把计数减一。
实验:clone 和转回
use bytes::{BufMut, Bytes, BytesMut};
#[path = "../common.rs"]
mod common;
use common::*;
fn fresh() -> (BytesMut, *const u8) {
let mut buf = BytesMut::with_capacity(64);
buf.put_slice(b"aaabbbcccddd");
let base = buf.as_ptr();
(buf, base)
}
fn show(b: &BytesMut, base: *const u8) -> String {
format!("ptr={} len={} cap={}", at(b.as_ptr(), base, 64), b.len(), b.capacity())
}
fn main() {
// BytesMut::clone
let (buf, base) = fresh();
let (c, d) = count(|| buf.clone());
println!("{:<30} {d} {}", "clone", show(&c, base));
// KIND_VEC:freeze 再转回来
let (buf, base) = fresh();
let (b, d) = count(|| buf.freeze());
println!("{:<30} {d}", "KIND_VEC freeze");
let (m, d) = count(|| BytesMut::from(b));
println!("{:<30} {d} {}", " -> BytesMut::from", show(&m, base));
// KIND_ARC:另一半还活着
let (mut buf, base) = fresh();
let head = buf.split_to(6).freeze();
let (m, d) = count(|| BytesMut::from(head));
println!("{:<30} {d} {}", "KIND_ARC, other alive", show(&m, base));
drop(m);
drop(buf);
// KIND_ARC:另一半先 drop
let (mut buf, base) = fresh();
let head = buf.split_to(6).freeze();
drop(buf);
let (m, d) = count(|| BytesMut::from(head));
println!("{:<30} {d} {}", "KIND_ARC, other dropped", show(&m, base));
// 静态
let s = Bytes::from_static(b"static");
let (m, d) = count(|| BytesMut::from(s));
println!("{:<30} {d} len={} cap={}", "from_static", m.len(), m.capacity());
}
跑了 3 次,输出相同:
clone alloc+1 (12B) free+0 ptr=elsewhere len=12 cap=12
KIND_VEC freeze alloc+1 (24B) free+0
-> BytesMut::from alloc+0 (0B) free+1 ptr=base+0 len=12 cap=64
KIND_ARC, other alive alloc+1 (6B) free+0 ptr=elsewhere len=6 cap=6
KIND_ARC, other dropped alloc+0 (0B) free+0 ptr=base+0 len=6 cap=64
from_static alloc+1 (6B) free+0 len=6 cap=6
clone:分配 12 字节,指针在别处,容量 12,原来的 64 字节容量没跟过来。KIND_VEC一来一回:freeze分配 24 字节的Shared(len < cap),转回时唯一,释放这个Shared,Vec原样拿回来,容量还是 64。往返一趟净分配为 0,但中间确实多了一次分配和一次释放。KIND_ARC、另一半还活着:拷贝 6 字节,free+0,因为只是把计数从 2 减到 1,Shared还被buf拿着。KIND_ARC、另一半先 drop:0 分配 0 释放,ptr还是base+0,容量变成 64。head被split_to(6)切出来时容量只有 6,转回之后shared_v_to_mut按整块Vec重新算,buf原来占着的 58 字节归了它。这时没有别的句柄,这么做是安全的。from_static:静态数据不能写,只能拷贝。
unsplit:能并就并,不能就拷
unsplit 是两层:
pub fn unsplit(&mut self, other: BytesMut) {
if self.is_empty() {
*self = other;
return;
}
if let Err(other) = self.try_unsplit(other) {
self.extend_from_slice(other.as_ref());
}
}
try_unsplit 决定能不能 O(1) 合并:
pub fn try_unsplit(&mut self, other: BytesMut) -> Result<(), BytesMut> {
if other.capacity() == 0 {
return Ok(());
}
let ptr = unsafe { self.ptr.as_ptr().add(self.len) };
if ptr == other.ptr.as_ptr()
&& self.kind() == KIND_ARC
&& other.kind() == KIND_ARC
&& self.data == other.data
{
// Contiguous blocks, just combine directly
self.len += other.len;
self.cap += other.cap;
Ok(())
} else {
Err(other)
}
}
能合并要同时满足四个条件:
self数据的末尾就是other的开头:比的是self.ptr + self.len,不是self.ptr + self.cap。如果self后面还有没写的容量,两段数据中间就隔着空洞,不能直接拼。两个句柄的区间不重叠,所以这个等式成立时self.len一定等于self.cap,self.cap += other.cap不会算错。- 两边都是
KIND_ARC:KIND_VEC的BytesMut独占自己的Vec,不可能和另一个句柄共用一块分配。 - 同一个
Shared:self.data == other.data。两块不同的分配也可能恰好首尾相接,地址相等不够。 - 顺序是
self在前、other在后。条件 1 已经隐含了这一点:反过来调用,self.ptr + self.len不会等于other.ptr。
合并时只改 len 和 cap。other 被按值移进来,函数返回时 drop,Shared 的计数减一,这就是文档说的「just decreases a reference count and sets a few indices」。
other.capacity() == 0 时直接 Ok,不看它来自哪里:容量为 0 的句柄没有字节,并不并都一样,drop 掉就行。
capacity() == 0 这个判断原来是 is_empty()PR #502(「Keep capacity when unsplit on empty other buf」,1.1.0)把 try_unsplit 第一行的 other.is_empty() 改成了 other.capacity() == 0。PR 说明:从一次 split 并回来时,「if the other buf is empty, other is dropped, capacity lost, and anything new added to the initial buf leads to a reallocation」。也就是 buf.split_off(buf.len()) 切出一个长度 0、容量还很大的尾巴,并回来时被当成「空」直接丢掉,buf 的容量回不到原来的大小。改成看容量之后,只有真正没有容量的句柄才走捷径,上面实验里最后一个 tail(长度 0、容量 52)就是因此能把 52 字节容量并回来。这个 PR 同时加了测试 bytes_mut_unsplit_empty_other_keeps_capacity,断言并回来后 capacity() 是 64。CHANGELOG 记在 1.1.0:「Keep capacity when unsplit on empty other buf (#502)」。
unsplit 在 try_unsplit 前面还有一个捷径:self 为空时直接 *self = other。这里看的是 self 的长度,不是容量:self 自己的容量随旧句柄一起 drop 掉。
try_unsplit 失败时,unsplit 退回 extend_from_slice:先 reserve(other.len()),再把 other 的字节拷到 self 的末尾。这一步是不是分配,取决于 self 剩下的容量和 reserve 的规则(见 reserve)。注意这时 other 还活着(它在 Err 里被交回来,拷完才 drop),所以 reserve 看到的 Shared 计数至少是 2。
实验:各种不相邻的 unsplit
use bytes::{BufMut, BytesMut};
#[path = "../common.rs"]
mod common;
use common::*;
// base 是最初那块缓冲的 (起点, 容量)
fn fresh(cap: usize) -> (BytesMut, (*const u8, usize)) {
let mut buf = BytesMut::with_capacity(cap);
buf.put_slice(b"aaabbbcccddd");
let base = (buf.as_ptr(), buf.capacity());
(buf, base)
}
fn show(b: &BytesMut, base: (*const u8, usize)) -> String {
format!("{:?} ptr={} len={} cap={}",
std::str::from_utf8(b).unwrap(), at(b.as_ptr(), base.0, base.1), b.len(), b.capacity())
}
fn main() {
// 1. 相邻:split_off 出去再并回来
let (mut buf, base) = fresh(64);
let tail = buf.split_off(6);
let (_, d) = count(|| buf.unsplit(tail));
println!("{:<24} {d} {}", "contiguous", show(&buf, base));
// 2. 顺序反了:后半段 unsplit 前半段,后面还有空位
let (mut buf, base) = fresh(64);
let head = buf.split_to(6);
let (_, d) = count(|| buf.unsplit(head));
println!("{:<24} {d} {}", "reversed, spare cap", show(&buf, base));
// 3. 顺序反了,而且没有空位(len == cap)
let (mut buf, base) = fresh(12);
let head = buf.split_to(6);
let (_, d) = count(|| buf.unsplit(head));
println!("{:<24} {d} {}", "reversed, full", show(&buf, base));
// 4. 中间隔着一段
let (mut buf, base) = fresh(64);
let mut a = buf.split_to(3);
let _gap = buf.split_to(3);
let (_, d) = count(|| a.unsplit(buf));
println!("{:<24} {d} {}", "gap in between", show(&a, base));
// 5. 两块不同的分配
let (mut x, base) = fresh(12);
let y = BytesMut::from(&b"zz"[..]);
let (_, d) = count(|| x.unsplit(y));
println!("{:<24} {d} {}", "different allocation", show(&x, base));
// 6. self 是空的:直接换成 other
let (mut buf, base) = fresh(64);
let all = buf.split();
let (_, d) = count(|| buf.unsplit(all));
println!("{:<24} {d} {}", "self empty", show(&buf, base));
// 反过来并:all 在前,buf 在后
let (mut buf, base) = fresh(64);
let mut all = buf.split();
let (_, d) = count(|| all.unsplit(buf));
println!("{:<24} {d} {}", "other empty, cap 52", show(&all, base));
}
跑了 3 次,输出相同:
contiguous alloc+0 (0B) free+0 "aaabbbcccddd" ptr=base+0 len=12 cap=64
reversed, spare cap alloc+0 (0B) free+0 "cccdddaaabbb" ptr=base+6 len=12 cap=58
reversed, full alloc+1 (12B) free+2 "cccdddaaabbb" ptr=elsewhere len=12 cap=12
gap in between alloc+1 (9B) free+0 "aaacccddd" ptr=elsewhere len=9 cap=9
different allocation alloc+1 (24B) free+2 "aaabbbcccdddzz" ptr=elsewhere len=14 cap=24
self empty alloc+0 (0B) free+0 "aaabbbcccddd" ptr=base+0 len=12 cap=12
other empty, cap 52 alloc+0 (0B) free+0 "aaabbbcccddd" ptr=base+0 len=12 cap=64
- contiguous:O(1),0 分配,回到
base+0、容量 64。 - reversed:
unsplit不会调整顺序,失败后是「追加」,内容变成cccdddaaabbb。后面有空位时(第 2 行)拷进buf自己的空闲容量,0 分配,ptr仍是base+6;没有空位时(第 3 行)buf是共享的KIND_ARC,reserve只能另分配一块(12 字节),把buf的数据拷过去,buf变回KIND_VEC,旧Shared的计数减一;再把head的字节追加进来。两次释放都发生在unsplit结束、head被 drop 的时候:计数到 0,释放旧的Vec和Shared。 - gap:
a后面隔着_gap,条件 1 不成立。分配 9 字节、0 释放:旧块还被_gap拿着。 - different allocation:
x从没 split 过,是KIND_VEC,条件 2 不成立。reserve走Vec::reserve,容量从 12 翻倍到 24。计数分配器没有覆盖realloc,GlobalAlloc的默认realloc是分配新块、拷贝、释放旧块,所以记成 1 次分配加 1 次释放;另一次释放是y。 - self empty:
buf长度为 0,直接被all替换,容量只剩all的 12,buf原来那 52 字节的容量随旧句柄丢掉了(只是看不到了,内存还在同一个Shared里,计数减一)。反过来让all.unsplit(buf),buf长度 0 但容量 52,走try_unsplit合并,容量回到 64。同样两个句柄,谁调用谁决定了结果。
use bytes::{BufMut, BytesMut};
#[path = "../common.rs"]
mod common;
use common::*;
fn main() {
let mut buf = BytesMut::with_capacity(64);
buf.put_slice(b"hello world");
let tail = buf.split_off(20);
println!("before: buf len={} cap={}, tail len={} cap={}",
buf.len(), buf.capacity(), tail.len(), tail.capacity());
let (_, d) = count(|| buf.unsplit(tail));
println!("unsplit: {d}");
println!("after: buf len={} cap={}", buf.len(), buf.capacity());
}
M2 Max、rustc 1.99.0,bytes = "=1.12.1"。tail 是从 buf 上 split_off 下来的,同一块分配、同一个 Shared,顺序也对。最后一行 buf 的 cap 是多少?
try_unsplit:把失败交还给调用方
try_unsplit 在 1.12.0 之前就存在,是 unsplit 内部用的私有函数。1.12.0 把它公开(PR #746),代码一行没改,只加了 pub 和文档。unsplit 的问题是失败时会悄悄拷贝,调用方无从知道;try_unsplit 把 other 原样放进 Err 还回来,由调用方决定是拷贝、换个顺序再试,还是另作处理。
实验:Err 的几种情况
use bytes::{BufMut, BytesMut};
#[path = "../common.rs"]
mod common;
use common::*;
fn fresh() -> BytesMut {
let mut buf = BytesMut::with_capacity(64);
buf.put_slice(b"aaabbbcccddd");
buf
}
fn report(name: &str, buf: &mut BytesMut, other: BytesMut) {
let other_ptr = other.as_ptr();
let (r, d) = count(|| buf.try_unsplit(other));
match r {
Ok(()) => println!("{name:<26} {d} Ok self len={} cap={}", buf.len(), buf.capacity()),
Err(o) => println!("{name:<26} {d} Err same handle={} len={} cap={} | self len={} cap={}",
o.as_ptr() == other_ptr, o.len(), o.capacity(), buf.len(), buf.capacity()),
}
}
fn main() {
// 1. 顺序反了
let mut buf = fresh();
let head = buf.split_to(6);
report("reversed", &mut buf, head);
// 2. 中间隔着一段
let mut buf = fresh();
let mut a = buf.split_to(3);
let _gap = buf.split_to(3);
report("gap in between", &mut a, buf);
// 3. split_off 的位置超过 len:self 后面有没写的容量
let mut buf = BytesMut::with_capacity(64);
buf.put_slice(b"aaabbb");
let tail = buf.split_off(10);
report("split_off past len", &mut buf, tail);
// 4. self 从没 split 过(KIND_VEC),other 来自别处
let mut x = fresh();
report("self never split", &mut x, BytesMut::from(&b"zz"[..]));
// 5. 两边各自 split 过,但不是同一块分配
let mut x = fresh();
let _x2 = x.split_off(12);
let mut y = fresh();
let y2 = y.split_off(0);
report("two different Shared", &mut x, y2);
// 6. other 容量为 0:直接 Ok,哪怕它来自别处
let mut x = fresh();
report("other cap 0 (new())", &mut x, BytesMut::new());
}
跑了 3 次,输出相同:
reversed alloc+0 (0B) free+0 Err same handle=true len=6 cap=6 | self len=6 cap=58
gap in between alloc+0 (0B) free+0 Err same handle=true len=6 cap=58 | self len=3 cap=3
split_off past len alloc+0 (0B) free+0 Err same handle=true len=0 cap=54 | self len=6 cap=10
self never split alloc+0 (0B) free+0 Err same handle=true len=2 cap=2 | self len=12 cap=64
two different Shared alloc+0 (0B) free+0 Err same handle=true len=12 cap=64 | self len=12 cap=12
other cap 0 (new()) alloc+0 (0B) free+0 Ok self len=12 cap=64
- 所有
Err都是 0 分配 0 释放,交回来的就是传进去的那个句柄(同一个指针、同样的长度和容量),self也没被改动。 - 前三行对应条件 1:顺序反、中间有空洞、
self后面有没写的容量(split_off(10)之后self是len=6 cap=10,数据末尾在 6,other从 10 开始)。第 4 行对应条件 2,第 5 行对应条件 3:两块缓冲各自提升出自己的Shared。 - 最后一行:
BytesMut::new()容量为 0,try_unsplit第一行就返回Ok,self不变。所以Ok的意思是「other已经被吸收或者本来就没东西」,不保证它来自同一块缓冲。
PR #746(「Make BytesMut::try_unsplit method public」,1.12.0)说明自己「is identical diff-wise to #513」,之前没被合并,讨论分散在 Bytes 要不要也有 try_unsplit(#287)和 BytesMut 要不要有(#702)两处。这个 PR 的论证是:BytesMut 已经公开了 unsplit,它的规格本身就是「Absorbs a BytesMut that was previously split off if they are contiguous, otherwise appends its bytes to this BytesMut」,于是「all this PR is doing is offering what unsplit already does but without the "otherwise..." part, leaving it up to the user to select what happens next」,不会增加向后兼容的负担。改动本身就是上面那段代码前面加了 pub 和文档,unsplit 的文档第一句也随之改成了现在的「if they are contiguous, otherwise appends its bytes」。CHANGELOG 1.12.0:「Add BytesMut::try_unsplit() (#746)」。
动脑筋答案
before: buf len=11 cap=20, tail len=0 cap=44
unsplit: alloc+0 (0B) free+0
after: buf len=11 cap=20
跑了 3 次,输出相同。容量是 20,不是 64:
split_off(20)之后buf是len=11 cap=20,tail从偏移 20 开始。try_unsplit比的是buf.ptr + buf.len,也就是偏移 11,不等于 20,条件 1 不成立,返回Err。unsplit退回extend_from_slice(tail),tail长度为 0,什么也没拷,reserve(0)也不分配。tail随后被 drop,计数减一,它那 44 字节的容量从buf的视角消失了。free+0说明没有释放内存:Shared还被buf拿着,整块 64 字节都在,只是没有句柄再看得到后面那 44 字节。
想把容量拿回来,split_off 的位置不能超过 len,或者先把中间那段写满。
小结
split_to/split_off/split都是「shallow_clone一份,再把两个句柄改成不重叠」。split_to切数据,检查at <= len;split_off切容量,检查at <= capacity,可以切在没写的区域。- 第一次 split 时
promote_to_shared(2)分配一个 40 字节的Shared,把整个Vec放进去,data从KIND_VEC变成KIND_ARC;之后的 split 只把计数加一。空的BytesMut(包括BytesMut::new())split 也会提升,这一点和Bytes不同。 - 越界的 panic 原文是
split_to out of bounds: 20 <= 11和split_off out of bounds: 65 <= 64,位置在bytes_mut.rs。 freeze:KIND_VEC拼回Vec走From<Vec<u8>>再advance(off);KIND_ARC不分配,换上bytes_mut.rs的SHARED_VTABLE。BytesMut::clone是深拷贝,只拷len字节,不带空闲容量。From<Bytes>唯一时复用缓冲(freeze出来的KIND_ARC句柄还会把容量算到整块末尾),不唯一时拷贝。unsplit只在「self数据末尾等于other开头、两边都是KIND_ARC、同一个Shared」时 O(1);否则追加拷贝,顺序反了不会被纠正。self为空时直接换成other。try_unsplit(1.12.0 公开,#746)失败时把原来的other放进Err交回,0 分配;other容量为 0 时总是Ok。