Rust in Everything
基于 bytes 1.12.1

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,以后只加计数

shallow_clone:

    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:两种形态两条路

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:

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。

📝NOTE
没有蠢问题

问: 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)
        }
    }

能合并要同时满足四个条件:

  1. self 数据的末尾就是 other 的开头:比的是 self.ptr + self.len,不是 self.ptr + self.cap。如果 self 后面还有没写的容量,两段数据中间就隔着空洞,不能直接拼。两个句柄的区间不重叠,所以这个等式成立时 self.len 一定等于 self.cap,self.cap += other.cap 不会算错。
  2. 两边都是 KIND_ARC:KIND_VEC 的 BytesMut 独占自己的 Vec,不可能和另一个句柄共用一块分配。
  3. 同一个 Shared:self.data == other.data。两块不同的分配也可能恰好首尾相接,地址相等不够。
  4. 顺序是 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 掉就行。

📝NOTE
冷知识: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。同样两个句柄,谁调用谁决定了结果。
❗IMPORTANT
动脑筋:split_off 切在长度之后,再 unsplit 回来,容量是多少?
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 已经被吸收或者本来就没东西」,不保证它来自同一块缓冲。
📝NOTE
冷知识:try_unsplit 是被「unsplit 已经在做这件事」说服公开的

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。

💬 关联讨论

发起讨论
加载中...