Rust in Everything
基于 bytes 1.12.1

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

常量和 kind:

// 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 字节。

📝NOTE
冷知识:bytes.rs 的 Shared 早有这条断言,bytes_mut.rs 的晚了好几年

两个文件各有一个 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 位):

位掩码 / 偏移内容
0KIND_MASK = 0b11,即 KIND_VEC
1—不用,始终是 0
2–4ORIGINAL_CAPACITY_MASK = 0b11100,ORIGINAL_CAPACITY_OFFSET = 2原始容量的编码 repr,0–7
5–63VEC_POS_OFFSET = 5pos:ptr 相对 Vec 起点前进了多少字节,最大 MAX_VEC_POS

NOT_VEC_POS_MASK = 0b11111 是低 5 位,改 pos 时用它保住 kind 和 repr。第 1 位在整个 bytes_mut.rs 里没有任何代码读写,ORIGINAL_CAPACITY_OFFSET 却是 2,不是 1。

📝NOTE
冷知识:第 1 位空着,是因为 kind 曾经要两位

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 >> 10repr还原值
0 – 1023000
1024 – 2047111024
2048 – 40952–322048
4096 – 81914–734096
8192 – 163838–1548192
16384 – 3276716–31516384
32768 – 6553532–63632768
65536 及以上64 及以上765536

还原值总是向下取到 2 的幂,最大 64 KiB。下面的实验会把每一行的 repr 从 data 里读出来,并让 reserve 实际用一次还原值。

📝NOTE
没有蠢问题

问: 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 = 576460752303423487512 PiB,没有哪块分配有这么大
32 位2^27 - 1 = 134217727128 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,这个坑在「常见坑」一章还会单独复现。

❗IMPORTANT
动脑筋:3000 字节的缓冲前进 40 字节之后,data 是多少?

平台 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 不会被原始容量托底。

💬 关联讨论

发起讨论
加载中...