Rust in Everything
基于 bytes 1.12.1

从零写一个最小 Bytes

主线位置:两条主线都自己走一遍。共享主线:Bytes 是四个字,clone 和 drop 读出 vtable 里的函数指针再调用,静态存储什么都不做,共享存储加减一个原子计数,slice 是 clone 加改 ptr / len;构建主线:BytesMut 用 data 的最低位区分 KIND_VEC 和 KIND_ARC,advance 把偏移记进 data,第一次 split_to 提升成共享块,freeze 按形态选一条路变成 Bytes。

flowchart TB
  stat["from_static"] --> vs["STATIC_VTABLE"]
  vec["From Vec"] --> sh["Shared:Vec 加计数"]
  sh --> vsh["SHARED_VTABLE"]
  wc["with_capacity"] --> kv["KIND_VEC"]
  kv -->|advance| kv
  kv -->|"第一次 split_to"| ka["KIND_ARC:指向 Shared"]
  ka -->|split_to| ka
  kv -->|freeze| vec
  ka -->|"freeze:换表"| vsh
  vs --> ops["clone、slice、drop"]
  vsh --> ops

本页只讲思路和关键代码。mini 是一个 264 行的库(去掉空行和注释约 185 行),不依赖 bytes;另有一个对照程序,把同一串操作分别交给 mini 和 bytes,用计数分配器逐步数分配次数。只有对照程序依赖 bytes:

[dependencies]
bytes = "=1.12.1"

mini 只有两种存储:'static 切片和一个引用计数的共享块。可提升存储、reserve 的完整实现和配套作业放在付费课程里。

Bytes:四个字,两个函数

pub struct Bytes {
  ptr: *const u8,
  len: usize,
  // 交给 vtable 解释:静态是空指针,共享是 Shared 的地址。
  // 没有可提升存储,没人会 CAS 它,所以不用 AtomicPtr
  data: *mut (),
  vtable: &'static Vtable,
}

struct Vtable {
  clone: unsafe fn(*mut (), *const u8, usize) -> Bytes,
  drop: unsafe fn(*mut (), *const u8, usize),
}

// SAFETY: 每张表自己保证:静态数据只读;共享块只读,计数是原子的
unsafe impl Send for Bytes {}
unsafe impl Sync for Bytes {}

impl Clone for Bytes {
  fn clone(&self) -> Bytes {
    // SAFETY: data、ptr、len 和 vtable 是同一个构造者给出的一套
    unsafe { (self.vtable.clone)(self.data, self.ptr, self.len) }
  }
}

impl Drop for Bytes {
  fn drop(&mut self) {
    // SAFETY: 同上;之后不再使用 self
    unsafe { (self.vtable.drop)(self.data, self.ptr, self.len) }
  }
}

Deref 是 slice::from_raw_parts(self.ptr, self.len),不经过 vtable。和 bytes 的 Bytes、Vtable 对一下:

  • 字段一样,data 的类型不一样。 bytes 的 data 是 AtomicPtr<()>,因为可提升存储第一次 clone 时要通过 &self 把它 CAS 成 Shared 的地址(clone 与提升)。mini 没有这种存储,data 构造之后不会再变,&self 下读一个普通指针就够了。
  • 表里只有两个函数。 bytes 0.5.0 引入 vtable 时也只有 clone 和 drop,into_vec、is_unique、into_mut 是 1.2.0 到 1.7.0 陆续加的(结构与 Vtable)。mini 不支持转回 Vec / BytesMut,不需要它们。
  • drop 按值拿 data。 这是 bytes 1.12.0(#826)改成的样子:不把 &mut self.data 交给间接调用,LLVM 就不会认为 Bytes 的地址被捕获(结构与 Vtable)。mini 的 clone 也按值拿,因为它的 data 不需要原子操作。
  • Send / Sync 同样是 unsafe impl。 编译器不知道 data 指着什么,承诺只能由每张表来守。

'static:两个空函数

impl Bytes {
  pub const fn from_static(s: &'static [u8]) -> Bytes {
    Bytes { ptr: s.as_ptr(), len: s.len(), data: ptr::null_mut(), vtable: &STATIC_VTABLE }
  }
}

const STATIC_VTABLE: Vtable = Vtable { clone: static_clone, drop: static_drop };

unsafe fn static_clone(_: *mut (), ptr: *const u8, len: usize) -> Bytes {
  Bytes { ptr, len, data: ptr::null_mut(), vtable: &STATIC_VTABLE }
}

unsafe fn static_drop(_: *mut (), _: *const u8, _: usize) {}

这就是 vtable 让静态数据零分配的全部原因:data 不必指向一个对象,表里的函数知道它是空的,clone 复制四个字,drop 什么都不做。用 Arc<dyn AsRef<[u8]>> 写同样的东西,包一个 &'static [u8] 也要先分配 Arc 的计数块(结构与 Vtable 里的 DynBytes 实测 1 次)。

STATIC_VTABLE 写成 const 而不是 static,和 bytes 的 STATIC_VTABLE 一样:from_static 是 const fn,&STATIC_VTABLE 被提升成 'static 引用。代价也一样,这张表没有唯一地址,不能拿 vtable 指针比较来判断「是不是静态存储」(五种存储)。

共享:一个 Vec 加一个原子计数

struct Shared {
  #[allow(dead_code)] // 从不读,只靠它的 Drop 释放缓冲
  vec: Vec<u8>,
  ref_cnt: AtomicUsize,
}

// BytesMut 拿 Shared 地址的最低位当 KIND_ARC,所以对齐必须是偶数
const _: () = assert!(align_of::<Shared>() % 2 == 0);

const SHARED_VTABLE: Vtable = Vtable { clone: shared_clone, drop: shared_drop };

unsafe fn increment_shared(shared: *mut Shared) {
  // 能 clone 说明自己持有一份,计数不会在这期间归零,Relaxed 就够
  let old = unsafe { (*shared).ref_cnt.fetch_add(1, Relaxed) };
  if old > isize::MAX as usize {
    std::process::abort();
  }
}

unsafe fn release_shared(shared: *mut Shared) {
  // Release:本句柄对数据的读都排在减一之前
  if unsafe { (*shared).ref_cnt.fetch_sub(1, Release) } != 1 {
    return;
  }
  // 最后一个:Acquire 读和所有 Release 配对,再释放 Vec 和 Shared
  unsafe { (*shared).ref_cnt.load(Acquire) };
  drop(unsafe { Box::from_raw(shared) });
}

impl From<Vec<u8>> for Bytes {
  /// 总是分配一个 Shared。bytes 在 len == cap 时不分配(可提升存储)
  fn from(vec: Vec<u8>) -> Bytes {
    let (ptr, len) = (vec.as_ptr(), vec.len());
    let shared = Box::into_raw(Box::new(Shared { vec, ref_cnt: AtomicUsize::new(1) }));
    Bytes { ptr, len, data: shared.cast(), vtable: &SHARED_VTABLE }
  }
}

shared_clone 调 increment_shared 再返回一个挂同一张表的新句柄,shared_drop 调 release_shared。计数的四步照抄 bytes 的 shallow_clone_arc 和 release_shared,它们又照抄标准库的 Arc:Relaxed 加一、超过 isize::MAX 就 abort、Release 减一、最后一个用 Acquire 读一次再释放。bytes 用 load 而不用 fence(Acquire) 是为了 TSAN,mini 照做(clone 与提升 的冷知识)。

Vec 被挪进 Box 之后,它的堆缓冲没有动,所以构造前取的 ptr 仍然有效。Shared 的 drop 就是 Vec 的 drop:u8 没有析构,Vec 按容量释放缓冲,vec.len() 是多少无所谓。

slice 是 clone 加改 ptr / len:

  pub fn slice(&self, range: Range<usize>) -> Bytes {
    assert!(range.start <= range.end, "range start must not be greater than end");
    assert!(range.end <= self.len, "range end out of bounds");
    let mut ret = self.clone();
    // SAFETY: range 在 [0, len] 之内
    ret.ptr = unsafe { ret.ptr.add(range.start) };
    ret.len = range.end - range.start;
    ret
  }

bytes 的 slice 多两件事:结果为空时不 clone、返回一个地址保留在原处的静态空 Bytes;range 可以是任意 RangeBounds(切片)。

BytesMut:data 的最低位

const KIND_ARC: usize = 0b0; // data 是 Shared 的地址
const KIND_VEC: usize = 0b1; // data 是 (pos << 1) | 1,独占一个 Vec
const KIND_MASK: usize = 0b1;
const VEC_POS_OFFSET: usize = 1;

pub struct BytesMut {
  ptr: NonNull<u8>,
  len: usize,
  cap: usize,
  data: *mut Shared,
}

with_capacity 用 ManuallyDrop 拿下一个 Vec 的三个字段,data 设成 ptr::without_provenance_mut(KIND_VEC):一个不指向任何东西的整数,pos 为 0。和 bytes 的 KIND_* 比,mini 少了原始容量的 3 位,因为没有 reserve 要用它,所以 pos 从第 1 位开始,而不是第 5 位(data 字段的编码)。

KIND_VEC 时 advance 要把偏移记下来,因为 ptr 往后挪了,释放时得把 Vec 的起点还给分配器:

  pub fn advance(&mut self, n: usize) {
    assert!(n <= self.len, "advance out of bounds");
    if self.kind() == KIND_VEC {
      // pos 不超过分配大小(≤ isize::MAX),左移一位不会溢出
      let pos = self.vec_pos() + n;
      self.data = ptr::without_provenance_mut((pos << VEC_POS_OFFSET) | KIND_VEC);
    }
    // SAFETY: n <= len <= cap
    self.ptr = unsafe { self.ptr.add(n) };
    self.len -= n;
    self.cap -= n;
  }

KIND_ARC 时不用记:偏移可以用 ptr 减去 Shared 里 Vec 的起点现算(mini 用不到,bytes 的 reserve 用)。bytes 的 pos 只有 59 位(32 位平台上 27 位),超过时提升成 Shared;mini 只让出 1 位,留下的 63 位放得下任何一块分配的大小,所以没有这条路。

drop、freeze、提升都要把 Vec 拼回来,用的是同一个函数,和 bytes 的 rebuild_vec 一样:

unsafe fn rebuild_vec(ptr: *mut u8, len: usize, cap: usize, off: usize) -> Vec<u8> {
  unsafe { Vec::from_raw_parts(ptr.sub(off), off + len, off + cap) }
}

split_to:第一次时提升

  pub fn split_to(&mut self, at: usize) -> BytesMut {
    assert!(at <= self.len, "split_to out of bounds");
    if self.kind() == KIND_ARC {
      // SAFETY: KIND_ARC 时 data 是活着的 Shared
      unsafe { increment_shared(self.data) };
    } else {
      self.promote_to_shared(2); // 一份给 self,一份给返回值
    }
    let other = BytesMut { ptr: self.ptr, len: at, cap: at, data: self.data };
    // SAFETY: at <= len <= cap;两个句柄从此看不重叠的两段
    self.ptr = unsafe { self.ptr.add(at) };
    self.len -= at;
    self.cap -= at;
    other
  }

  fn promote_to_shared(&mut self, ref_cnt: usize) {
    // 先分配再拼回 Vec:Box 分配失败展开时,缓冲仍只归 self 所有(bytes 1.12.1 的 #837)
    let uninit = Box::<Shared>::new_uninit();
    // SAFETY: KIND_VEC 时 ptr - pos 是 Vec 的起点,len、cap 加回 pos 就是原来的 Vec
    let vec = unsafe { rebuild_vec(self.ptr.as_ptr(), self.len, self.cap, self.vec_pos()) };
    let shared = Box::write(uninit, Shared { vec, ref_cnt: AtomicUsize::new(ref_cnt) });
    self.data = Box::into_raw(shared); // 偶数对齐,最低位天然是 KIND_ARC
  }
  • 两段不重叠是安全的根据。 返回值的 cap 被截成 at,self 从 at 开始,两个可写句柄共用一块分配却永远写不到对方的区间。bytes 的 split_to 也是先 shallow_clone 一份再各改各的(split 与 freeze)。
  • 计数从 2 开始。 提升的那一刻已经有两个句柄。
  • 不需要标记位。 Box::into_raw 给出的地址按 Shared 的对齐(8)排,最低位一定是 0,就是 KIND_ARC;文件开头那条 const 断言守着这个前提,bytes 有同样一条(data 字段的编码)。
  • 先分配,再拼 Vec。 如果先 rebuild_vec 再 Box::new(Shared { vec, .. }),Box::new 展开时 vec 会被 drop,而 self 仍是 KIND_VEC,之后 self 的 drop 再释放一次同一块缓冲。bytes 1.12.1 唯一的修复(#837)就是把 promote_to_shared、shallow_clone_vec、From<Vec<u8>> 三处改成先分配的顺序(怎么读这一卷)。
  • at 是 0 也提升。 和 bytes 的 BytesMut 一样,下面的动脑筋会量到。

freeze:两条路

  pub fn freeze(self) -> Bytes {
    let me = ManuallyDrop::new(self);
    if me.kind() == KIND_ARC {
      // 计数原样交给 Bytes:同一个 Shared,换一张 vtable
      return Bytes { ptr: me.ptr.as_ptr(), len: me.len, data: me.data.cast(), vtable: &SHARED_VTABLE };
    }
    let off = me.vec_pos();
    // SAFETY: 同 promote_to_shared;me 不会再 drop
    let vec = unsafe { rebuild_vec(me.ptr.as_ptr(), me.len, me.cap, off) };
    let mut b = Bytes::from(vec); // 分配一个 Shared
    // SAFETY: off <= vec.len(),跳过前面已经 advance 掉的部分
    b.ptr = unsafe { b.ptr.add(off) };
    b.len -= off;
    b
  }

和 bytes 的 freeze 是同一个形状:KIND_ARC 不分配,把这个句柄的那份计数交给 Bytes;KIND_VEC 拼回 Vec,交给 From<Vec<u8>>,再跳过前缀。BytesMut 的 Drop 也分两路:KIND_VEC 拼回 Vec 让它自己释放,KIND_ARC 调 release_shared。

📝NOTE
没有蠢问题

问: mini 的 Bytes 和 BytesMut 共用一个 Shared,KIND_ARC 的 freeze 直接挂 SHARED_VTABLE。bytes 为什么要两个 Shared、两张共享表?

答: 两边要记的东西不一样。bytes.rs 的 Shared 是 buf、cap、ref_cnt,注释说它只「Holds arguments to dealloc upon Drop」,24 字节,From<Vec<u8>> 和可提升存储的提升都建它;bytes_mut.rs 的 Shared 是 vec、original_capacity_repr、ref_count,40 字节,多出的原始容量是给共享时的 reserve 用的(reserve)。冻结出来的 Bytes 还指着后一种,所以要挂 bytes_mut.rs 的那张表,表里的函数按 bytes_mut::Shared 的布局读计数。mini 没有 reserve,也就没有原始容量,一种 Shared 够用。bytes 曾经在这里填错过一个函数,把一种 Shared 当成另一种读(#718,见 clone 与提升 的冷知识)。

跑一下:和 bytes 比分配次数

对照程序 compare.rs 用一个宏把同一串操作写一遍,分别展开成 mini 的类型和 bytes 的类型。计数分配器的写法和 bytes 解决什么问题 里的一样,多一个记存活字节的计数;它记下每一步的分配次数、分配字节数和释放次数,操作的结果在计数结束之后才 drop。put_slice 和 advance 在 mini 上是固有方法,在 bytes 上来自 BufMut / Buf:

type Count = (usize, usize, usize); // (分配次数, 分配字节, 释放次数)

// 跑 f,返回 (结果, 这一步的计数);结果在计数之后才由调用方 drop
fn step<R>(f: impl FnOnce() -> R) -> (R, Count) {
  let (a, b, d) = (ALLOCS.load(Relaxed), BYTES.load(Relaxed), FREES.load(Relaxed));
  let r = f();
  (r, (ALLOCS.load(Relaxed) - a, BYTES.load(Relaxed) - b, FREES.load(Relaxed) - d))
}

// 同一段代码,$b / $bm 换成 mini 或 bytes 的类型;返回每一步的计数和最后的内容
macro_rules! scenario {
  ($b:ty, $bm:ty) => {{
    // ...
    let v = vec![7u8; 1024];
    let (b, r) = step(|| <$b>::from(v));
    rows.push(("from(Vec len==cap)", r));
    let (c1, r) = step(|| b.clone());
    rows.push(("  .clone() #1", r));
    // ...
    let (mut buf, r) = step(|| <$bm>::with_capacity(8192));
    rows.push(("BytesMut::with_capacity", r));
    let req = b"POST /upload HTTP/1.1\r\nHost: a\r\nContent-Length: 11\r\n\r\nhello world";
    let (_, r) = step(|| buf.put_slice(&req[..]));
    rows.push(("  put_slice(65)", r));
    let (head, r) = step(|| buf.split_to(54));
    rows.push(("  split_to(54)", r));
    let (head, r) = step(|| head.freeze());
    rows.push(("  head.freeze()", r));
    // ...
  }};
}

完整的操作序列就是下面输出的第一列:静态切片 clone;len == cap 的 Vec 转过来 clone 两次、切一段;len < cap 的 Vec 转过来;照 一块缓冲的一生 那条 hyper 读路径切出请求头和 body;最后一个 advance(4) 之后的 KIND_VEC 缓冲 freeze。每个切出来的值算一个不分配的校验和,两边比对内容。M2 Max、rustc 1.99.0,cargo run --release --bin compare,跑 3 次,输出逐字节相同:

------------------------------------------------------------------
step                                    mini        bytes 1.12.1
from_static                          0(0B)/0             0(0B)/0
  .clone()                           0(0B)/0             0(0B)/0
  drop both                          0(0B)/0             0(0B)/0
from(Vec len==cap)                  1(32B)/0             0(0B)/0  *
  .clone() #1                        0(0B)/0            1(24B)/0  *
  .clone() #2                        0(0B)/0             0(0B)/0
  .slice(10..20)                     0(0B)/0             0(0B)/0
  drop all four                      0(0B)/2             0(0B)/2
from(Vec len<cap)                   1(32B)/0            1(24B)/0  *
  drop                               0(0B)/2             0(0B)/2
BytesMut::with_capacity           1(8192B)/0          1(8192B)/0
  put_slice(65)                      0(0B)/0             0(0B)/0
  split_to(54)                      1(32B)/0            1(40B)/0  *
  head.freeze()                      0(0B)/0             0(0B)/0
  head.slice(5..12)                  0(0B)/0             0(0B)/0
  split_to(11).freeze()              0(0B)/0             0(0B)/0
  drop uri, head, body               0(0B)/0             0(0B)/0
  drop buf                           0(0B)/2             0(0B)/2
KIND_VEC advance(4)                  0(0B)/0             0(0B)/0
  .freeze() (len<cap)               1(32B)/0            1(24B)/0  *
  drop                               0(0B)/2             0(0B)/2
contents equal: true
live bytes after scenario: mini=0 bytes=0
size_of Bytes: mini=32 bytes=32; BytesMut: mini=32 bytes=32

每格是「分配次数(字节)/ 释放次数」,第一行横线是为了让标准输出的缓冲先分配好。带 * 的行两边不同:

步骤minibytes为什么
from(Vec len==cap) 与第一次 clone构造时 1 次第一次 clone 时 1 次bytes 走可提升存储,计数块等到第一次 clone 才分配(clone 与提升);mini 构造时就分配。两边都只分配一次
from(Vec len<cap)、KIND_VEC 的 freeze32 字节24 字节mini 的 Shared 是 Vec(3 个字)加计数;bytes 这里用 bytes.rs 的 Shared,buf、cap、ref_cnt 三个字
split_to(54)32 字节40 字节bytes 的 bytes_mut::Shared 多一个 original_capacity_repr

其余每一行都相同:静态切片从头到尾 0 次分配;共享存储的 clone、slice、KIND_ARC 的 freeze、第二次 split_to 都是 0 次;最后一个句柄 drop 时都是 2 次释放(缓冲和计数块);场景结束时两边的存活字节都回到 0,内容的校验和一致,两种类型都是 32 字节。

再用 8 个线程对 mini 的计数做一次压力测试:每轮建一个 4 KiB 的 BytesMut,split_to(2048).freeze() 和剩下的一半 freeze() 得到两个共享同一块 Shared 的 Bytes,8 个线程各自 slice / clone / drop 100 次并检查内容,跑 1000 轮,每轮结束看存活字节有没有回到第 0 轮的值。release 跑 3 次、debug 跑 1 次,输出相同:

(stdout warmed up)
rounds=1000 threads=8 warm-up live=0B, later rounds with different live: 0

这只说明在这台机器上没观察到泄漏或重复释放,不证明代码正确。mini 里每一处 unsafe 都应该在 miri 下跑一遍(cargo +nightly miri run --bin compare),它能查出越界、释放后使用、违反别名规则和 provenance 的问题;bytes 自己的 CI 就在 miri 下跑全部测试(源码地图)。本机的 nightly 没装 miri(cargo +nightly miri --version 报 'cargo-miri' is not installed),本页的代码没有在 miri 下跑过。mini 也特意用 ptr::without_provenance_mut 造 KIND_VEC 的 data,而不是 usize as *mut _,就是为了在 strict provenance 下也说得通。

❗IMPORTANT
动脑筋:容量为 0 的 BytesMut,split_to(0) 分配几次?

M2 Max、rustc 1.99.0,--release。计数分配器同上,只数次数和字节:

fn main() {
  let (mut m, a, b) = count(|| exp_bytes_handson::BytesMut::with_capacity(0));
  println!("mini  with_capacity(0): allocs={a} ({b}B)");
  let (e, a, b) = count(|| m.split_to(0));
  println!("mini  split_to(0):      allocs={a} ({b}B)");
  drop((e, m));

  let (mut m, a, b) = count(|| bytes::BytesMut::with_capacity(0));
  println!("bytes with_capacity(0): allocs={a} ({b}B)");
  let (e, a, b) = count(|| m.split_to(0));
  println!("bytes split_to(0):      allocs={a} ({b}B)");
  drop((e, m));
}

一个字节都没有、连缓冲都没分配的 BytesMut,切出一个空段,mini 和 bytes 各分配几次、多少字节?

和 bytes 比:少了什么

mini 能把两条主线跑通,离 bytes 还差很远。下表每一行是 mini 缺的一样东西:

缺的mini 现在怎样bytes 怎么做见
可提升存储From<Vec<u8>> 总是分配 Sharedlen == cap 时转 Box<[u8]>,奇偶两张表,第一次 clone 时 CAS 提升五种存储、clone 与提升
其他存储只有静态和共享from_owner 的 Owned<T>,每个 T 一张表五种存储
转回去不能into_vec、into_mut、is_unique、try_into_mut,唯一时复用缓冲clone 与提升
空切片照常 clone结果为空时不 clone,返回地址保留在原处的静态空 Bytes切片
切片的其他形式只有 slicesplit_to、split_off、truncate(可提升存储要先提升)、slice_ref切片
扩容没有,写满就 panicreserve_inner 原地挪回、唯一时扩大、共享时另起新 Vec;原始容量reserve
偏移溢出63 位,碰不到pos 超过 MAX_VEC_POS 时提升成 Shareddata 字段的编码
合并没有unsplit / try_unsplit,相邻且同一块时 O(1)split 与 freeze
读写游标两个固有方法Buf / BufMut,chunk_mut 交出 UninitSliceBuf、BufMut 与 UninitSlice
验证一个压力测试miri、ASAN、TSAN、loom、专用分配器的测试源码地图

前两行是 vtable 的另一半用处:mini 只有两张表,data 只有空指针和 Shared 地址两种含义;bytes 的 data 还可以是带标记的缓冲地址,并且要能被 CAS,这才是它用 AtomicPtr 的原因。可提升存储、reserve 的完整实现和配套作业在付费课程里。

动脑筋答案

跑 3 次,输出相同:

mini  with_capacity(0): allocs=0 (0B)
mini  split_to(0):      allocs=1 (32B)
bytes with_capacity(0): allocs=0 (0B)
bytes split_to(0):      allocs=1 (40B)
  • with_capacity(0) 两边都不分配:Vec::with_capacity(0) 只给一个悬空的非空指针。
  • split_to(0) 两边都分配 1 次:mini 的 32 字节是 Vec 加计数,bytes 的 40 字节多一个 original_capacity_repr。split_to 不看 at,只要是 KIND_VEC 就先提升,为一个空缓冲建了一个共享块。
  • 这一点 bytes 的 Bytes 不一样:Bytes::split_to 切出空段时不 clone,也就不会提升(切片);BytesMut 没有这个特判(data 字段的编码)。

小结

  • Bytes 是四个字,clone 和 drop 读出 vtable 里的函数指针再调用。静态存储的两个函数什么都不做,所以 from_static 和它的 clone 都是 0 次分配;共享存储是一个 Vec 加一个照抄 Arc 的原子计数。
  • mini 的 data 是普通指针,因为它没有可提升存储、没有人会 CAS 它;bytes 用 AtomicPtr,正是为了可提升存储第一次 clone 时的那次 CAS。
  • BytesMut 的 data 最低位 1 是 KIND_VEC(其余位是偏移),0 是 KIND_ARC(Shared 地址,靠偶数对齐)。第一次 split_to 先分配 Shared 再拼回 Vec,计数从 2 开始;freeze 在 KIND_ARC 时只换一张表,在 KIND_VEC 时走 From<Vec<u8>>。
  • 和 bytes 跑同一串操作,分配次数只在可提升存储那几步不同(mini 构造时分配,bytes 第一次 clone 时分配,总数一样),其余只差 Shared 的大小;场景结束时两边的存活字节都是 0。
  • 代码应该在 miri 下跑一遍,本机没装 miri,没有跑过;8 线程的压力测试只说明没观察到泄漏。

💬 关联讨论

发起讨论
加载中...