Rust in Everything
基于 bytes 1.12.1

bytes 解决什么问题

主线位置:两条主线的起点。本页用一个计数分配器说明为什么需要 Bytes(共享主线)和 BytesMut(构建主线)这两个类型,以及 Buf / BufMut 在两条线上分别管什么。机制本身留给 02、03、04 三部分。

flowchart TB
  buf["一块 64 KiB 缓冲"] -->|"to_vec 每帧拷贝"| vecs["100 个 Vec"]
  buf -->|"Arc::from 整块拷贝"| arcr["Arc 加下标"]
  buf -->|"split_to 不拷贝"| bys["100 个 Bytes"]
  bys --> shared["共用一个 Shared"]
  arcr --> owners["100 个所有者"]
  vecs --> owners
  shared --> owners

lib.rs 开头的文档用一句话说了 Bytes 的用途:

Bytes values facilitate zero-copy network programming by allowing multiple Bytes objects to point to the same underlying memory. This is managed by using a reference count to track when the memory is no longer needed and can be freed.

紧接着是一个例子:往一个 BytesMut::with_capacity(1024) 里写 13 字节、split 走,再写 13 字节、再 split 走,最后断言 buf.capacity() 是 998。文档的解释是「only a single buffer of 1024 is allocated」,两个切出去的值 a、b 和 buf 共用这一块,各自只记着自己能看到的那一段。这一页就把这件事放大,用分配次数说清楚它省下了什么。

问题:一块缓冲,很多所有者

网络程序读数据的方式几乎都一样:准备一块缓冲,系统调用往里填,填进来的字节里可能有好几条消息,也可能只有半条。解析器从里面认出一帧帧的边界,再把每一帧交出去:交给另一个任务、放进通道、存进 HeaderMap、作为 HTTP body 的一块返回给用户。

用借用 &buf[a..b] 交出去不行,有两个原因:

  • 接收方经常要求 'static。tokio::spawn 的 future、通道里的消息都不能借用一个局部变量。
  • 读缓冲还要接着用。下一次 read 要拿 &mut buf 往后面写,而前面借出去的 &[u8] 还活着,借用检查器不会允许。

所以每一帧都得是一个有所有权的值。问题变成:怎样让 100 个有所有权的值指向同一块内存的 100 个不同区间,并且在最后一个值消失时才释放这块内存。

三种做法的代价

拿一个具体的任务比较:64 KiB 的缓冲,按 656 字节切成 100 帧(最后一帧 592 字节),每帧交给一个所有者,然后全部 drop。用一个计数用的全局分配器记下切分这一步的分配次数和字节数,再看每一帧的地址是不是还在原来那块缓冲里,不在就说明它被拷贝过。

bytes = "=1.12.1"

计数分配器和打印用的几个小函数放在 src/common.rs,下一页的程序也用它:

#![allow(dead_code)]
use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering::Relaxed};

use bytes::{Bytes, BytesMut};

// 计数分配器:只数次数和字节,真正的分配交给 System
struct Counting;
static ALLOCS: AtomicUsize = AtomicUsize::new(0);
static ALLOC_BYTES: AtomicUsize = AtomicUsize::new(0);
static FREES: AtomicUsize = AtomicUsize::new(0);

unsafe impl GlobalAlloc for Counting {
  unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
    ALLOCS.fetch_add(1, Relaxed);
    ALLOC_BYTES.fetch_add(layout.size(), Relaxed);
    unsafe { System.alloc(layout) }
  }
  unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
    FREES.fetch_add(1, Relaxed);
    unsafe { System.dealloc(ptr, layout) }
  }
}

#[global_allocator]
static GLOBAL: Counting = Counting;

#[derive(Clone, Copy)]
pub struct Snap(pub usize, pub usize, pub usize);

pub fn snap() -> Snap {
  Snap(ALLOCS.load(Relaxed), ALLOC_BYTES.load(Relaxed), FREES.load(Relaxed))
}

// 这一步的分配增量。操作一结束就取,后面拼字符串、打印的分配不算进去
pub fn delta(before: Snap) -> Snap {
  let now = snap();
  Snap(now.0 - before.0, now.1 - before.1, now.2 - before.2)
}

// 打印,并返回打印之后的快照作为下一步的起点
pub fn report(step: &str, d: Snap, state: String) -> Snap {
  println!("{step:<22} alloc+{} ({}B) free+{}  {state}", d.0, d.1, d.2);
  drop(state);
  snap()
}

// 指针落在最初那块 8 KiB 里就打印偏移,否则是别的分配
fn loc(p: *const u8, base: usize) -> String {
  let p = p as usize;
  if base <= p && p <= base + 8192 { format!("base+{}", p - base) } else { "elsewhere".to_string() }
}

pub fn mut_state(name: &str, b: &BytesMut, base: usize) -> String {
  format!("{name}: ptr={} len={} cap={}", loc(b.as_ptr(), base), b.len(), b.capacity())
}

pub fn bytes_state(name: &str, b: &Bytes, base: usize) -> String {
  format!("{name}: ptr={} len={} unique={}", loc(b.as_ptr(), base), b.len(), b.is_unique())
}

src/bin/why.rs:

use std::ops::Range;
use std::sync::Arc;

use bytes::{Bytes, BytesMut};

#[path = "../common.rs"]
mod common;
use common::*;

const SIZE: usize = 64 * 1024;
const FRAME: usize = 656; // 65536 / 656 向上取整 = 100 帧

// 一次「从 socket 读到的」64 KiB,长度等于容量
fn input() -> Vec<u8> {
  let mut v = Vec::with_capacity(SIZE);
  v.extend((0..SIZE).map(|i| i as u8));
  v
}

// data 是切分前的缓冲,lo 是它的起始地址;只数 split 这一段的分配
fn measure<D, T>(
  name: &str,
  data: D,
  lo: usize,
  split: impl FnOnce(D, &mut Vec<T>),
  view: impl Fn(&T) -> &[u8],
) {
  let mut frames = Vec::with_capacity(100);
  let s = snap();
  split(data, &mut frames);
  let d = delta(s);
  // 帧的字节不在原缓冲 [lo, lo + SIZE) 里,就是被拷贝过的
  let copied: usize = frames
    .iter()
    .map(&view)
    .filter(|f| !(lo..lo + SIZE).contains(&(f.as_ptr() as usize)))
    .map(|f| f.len())
    .sum();
  let n = frames.len();
  let s = snap();
  frames.clear();
  let freed = delta(s);
  println!("{name:<26}{n:>7}{:>7}{:>12}{copied:>8}{:>15}", d.0, d.1, freed.2);
}

fn main() {
  println!("{:<26}{:>7}{:>7}{:>12}{:>8}{:>15}", "way", "frames", "allocs", "alloc_bytes", "copied", "frees_on_drop");

  // 1. 每个所有者一份自己的 Vec
  let data = input();
  let lo = data.as_ptr() as usize;
  measure("Vec<u8> to_vec", data, lo, |data, out| {
    out.extend(data.chunks(FRAME).map(|c| c.to_vec()));
  }, |f: &Vec<u8>| &f[..]);

  // 2. 每帧一个 Arc<[u8]>
  let data = input();
  let lo = data.as_ptr() as usize;
  measure("Arc<[u8]> per frame", data, lo, |data, out| {
    out.extend(data.chunks(FRAME).map(Arc::<[u8]>::from));
  }, |f: &Arc<[u8]>| &f[..]);

  // 3. 整块转成一个 Arc<[u8]>,每帧共享它、自己记范围
  let data = input();
  let lo = data.as_ptr() as usize;
  measure("Arc<[u8]> + Range", data, lo, |data, out| {
    let whole: Arc<[u8]> = Arc::from(data);
    out.extend((0..SIZE).step_by(FRAME).map(|i| (whole.clone(), i..(i + FRAME).min(SIZE))));
  }, |(a, r): &(Arc<[u8]>, Range<usize>)| &a[r.clone()]);

  // 4. Vec 转成 Bytes,再一段段 split_to
  let data = input();
  let lo = data.as_ptr() as usize;
  measure("Bytes split_to", data, lo, |data, out| {
    let mut rest = Bytes::from(data);
    while !rest.is_empty() {
      out.push(rest.split_to(FRAME.min(rest.len())));
    }
  }, |f: &Bytes| &f[..]);

  // 5. 直接读进 BytesMut,split_to + freeze
  let mut buf = BytesMut::with_capacity(SIZE);
  buf.extend_from_slice(&input());
  let lo = buf.as_ptr() as usize;
  measure("BytesMut split_to+freeze", buf, lo, |mut buf, out| {
    while !buf.is_empty() {
      out.push(buf.split_to(FRAME.min(buf.len())).freeze());
    }
  }, |f: &Bytes| &f[..]);
}

M2 Max、rustc 1.99.0,cargo run --release --bin why,跑 3 次,输出相同:

way                        frames allocs alloc_bytes  copied  frees_on_drop
Vec<u8> to_vec                100    100       65536   65536            100
Arc<[u8]> per frame           100    100       67136   65536            100
Arc<[u8]> + Range             100      1       65552   65536              1
Bytes split_to                100      1          24       0              2
BytesMut split_to+freeze      100      1          40       0              2

allocs 和 alloc_bytes 只算切分那一步;copied 是地址不在原缓冲里的帧的字节数之和;frees_on_drop 是把 100 帧全部 drop 时释放了几块内存。逐行看:

做法分配拷贝说明
Vec<u8>100 次,共 65536 字节64 KiB每帧一份独立的内存,互不相干,所以 drop 时也是 100 次释放
每帧一个 Arc<[u8]>100 次,共 67136 字节64 KiB多出的 1600 字节是每个 Arc 的强、弱两个计数,每个 16 字节
整块 Arc<[u8]> 加下标1 次,65552 字节64 KiB只有一块共享内存,但 Arc::from(Vec) 要把 64 KiB 搬进一块新分配
Bytes::from(Vec) 再 split_to1 次,24 字节0原缓冲原地被共享,只多了一个引用计数块
BytesMut 的 split_to + freeze1 次,40 字节0同上,计数块大一些

第三行说明光有引用计数不够。Arc<[u8]> 的计数和数据在同一块分配里,计数在前,数据在后,Vec 那块内存前面没有放计数的位置,所以 Arc::from(Vec<u8>) 只能新分配一块再拷贝。Bytes 的计数放在单独的一块小分配里,数据留在原地。

最后两行的 24 和 40 是两个不同的 Shared 结构。Bytes::from(Vec<u8>) 用的是 bytes.rs 里的 Shared(缓冲指针、容量、引用计数,3 个字);BytesMut 第一次 split_to 时建的是 bytes_mut.rs 里的 Shared(一个 Vec<u8>、original_capacity_repr、ref_count,5 个字)。两个类型各自怎么存这个计数、什么时候建它,分别在 五种存储 和 split 与 freeze。drop 时都是 2 次释放:数据缓冲一次,Shared 一次,发生在最后一帧被 drop 的时候。

💡TIP
打个比方:复印还是发借阅条

图书馆只有一本很厚的书(64 KiB 的缓冲),100 个读者各要其中几页。Vec<u8> 的做法是把每人要的那几页复印一份(to_vec),复印件归读者所有,书还回书架。Bytes 的做法是不复印,给每个读者发一张借阅条,上面写着「第几页到第几页」(ptr 和 len);前台的登记本记着这本书还有几张借阅条在外面(Shared 里的引用计数)。读者还条时登记本减一,减到零书才下架(释放缓冲)。

比方到这里为止:真实的借阅条之间可以互相复制(clone 只是登记本加一),而且拿着借阅条只能看、不能在书上写字。要写,得先拿到 BytesMut,那是一张「这几页只归我」的条子。

从 Vec 来的 Bytes:计数块什么时候分配

第四行里 Bytes::from(data) 用的 Vec 长度正好等于容量(input() 里 with_capacity(SIZE) 后填满)。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());
        // ...

长度等于容量时转成 Box<[u8]>,不分配;否则立刻分配一个 Shared。走 Box<[u8]> 那条路的 Bytes 叫「可提升」存储:一开始只有数据,没有计数,第一次 clone 时才补上。下面的动脑筋看的就是这两条路的分配时机。可提升存储的细节在 clone 与提升。

❗IMPORTANT
动脑筋:Bytes::from(v) 和它之后的第一次、第二次 split_to 各分配几次?

M2 Max、rustc 1.99.0。v 里有 65536 字节,分两种情况:容量 65536(等于长度)和容量 65537(多 1 字节)。common.rs 同上。

use bytes::Bytes;

#[path = "../common.rs"]
mod common;
use common::*;

fn run(cap: usize, label: &str) {
  let mut v = Vec::with_capacity(cap);
  v.extend_from_slice(&[7u8; 65536]);
  let base = v.as_ptr() as usize;
  let s = snap();
  let mut b = Bytes::from(v);
  let d = delta(s);
  let s = report(label, d, String::new());
  let first = b.split_to(656);
  let d = delta(s);
  let s = report("split_to #1", d, String::new());
  let second = b.split_to(656);
  let d = delta(s);
  report("split_to #2", d, format!("same buffer: {}", second.as_ptr() as usize == base + 656));
  drop((first, second, b));
}

fn main() {
  run(65536, "cap=65536 Bytes::from");
  run(65537, "cap=65537 Bytes::from");
}

Bytes 与 BytesMut 的分工

上面的第四、五行是两种不同的出发点:手里已经有一个填好的 Vec<u8>,还是从一开始就在 bytes 的类型里写。bytes 用两个类型分别对应「只读、到处共享」和「可写、唯一拥有」:

Bytes:

pub struct Bytes {
    ptr: *const u8,
    len: usize,
    // inlined "trait object"
    data: AtomicPtr<()>,
    vtable: &'static Vtable,
}

BytesMut:

pub struct BytesMut {
    ptr: NonNull<u8>,
    len: usize,
    cap: usize,
    data: *mut Shared,
}
BytesBytesMut
能不能写不能,只实现 Deref<Target = [u8]>能,还实现 DerefMut 和 BufMut
共享clone、slice、split_to 都只增加引用计数clone 是深拷贝(Clone 实现 是 BytesMut::from(&self[..]));split_to / split_off 切出的两段共用一块内存但互不重叠,各自可写
第三个字段data:存储相关的指针,含义由 vtable 决定cap:这一段之后还能写多少
背后的存储vtable 指向的五种之一,见 结构与 Vtable两种:还是一个 Vec(KIND_VEC),或者已经和别人共用一个 Shared(KIND_ARC),见 data 字段的编码
互相转换try_into_mut:is_unique() 为真时转回 BytesMut,否则原样放在 Err 里还给你freeze:转成 Bytes,不拷贝数据

Bytes 的 vtable 是一张手写的函数表(clone、into_vec、into_mut、is_unique、drop),所以同一个类型背后可以是 &'static [u8]、从 Vec 来的内存、BytesMut 冻结来的内存等等,clone 和 drop 时各走各的实现。BytesMut 不需要这么多变化,它把「是不是 Vec」和 Vec 的一些元数据塞进 data 指针的低位里。

两个结构都只有 4 个字。在 M2 Max 上用 size_of 量,Bytes、BytesMut 都是 32 字节,Option<Bytes> 也是 32(vtable 是引用,不会是空指针,None 用它表示);Vec<u8> 是 24,(Arc<[u8]>, Range<usize>) 也是 32。所以和上面第三行的手工做法相比,Bytes 并没有多占地方。

📝NOTE
没有蠢问题

问: 第三行的 Arc<[u8]> 加下标也只有一次分配。如果数据一开始就是我自己读进 Arc<[u8]> 的,不存在从 Vec 转换的拷贝,是不是就不需要 bytes 了?

答: 只看「共享 + 切片」这一件事,(Arc<[u8]>, Range<usize>) 确实够用,它和 Bytes 一样是 32 字节。差别在两头。读的那头:Arc<[u8]> 一旦建好就不能再往里写(Arc::get_mut 只在唯一时给出可变引用,而且给的是整块),没法像 BytesMut 那样「一边往后读、一边把前面切出去,等切出去的都释放了再回收整块」,下一页的程序就是这个过程。用的那头:每个收到帧的地方都要认识你的元组类型,而 hyper 交出的 body 块、axum 的 Bytes 提取器用的都是 Bytes(见 Incoming 与 body 通道 和 body 提取器)。Bytes 还能零分配地包住 &'static [u8](Bytes::from_static),这在 Arc<[u8]> 上做不到。

Buf 与 BufMut:把游标抽出来

文档的第二节讲两个 trait:

Buf and BufMut maintain cursors tracking the current position in the underlying byte storage. When bytes are read or written, the cursor is advanced.

Buf 必须实现的只有三个方法:remaining(还剩多少)、chunk(从当前位置开始的一段连续字节)、advance(游标前进)。get_u16、copy_to_bytes 这些几十个方法都是在这三个上面写的默认实现。BufMut 对称:remaining_mut、chunk_mut、advance_mut,put_u16、put_slice 是默认实现。

「游标」和「存储」分开有两个好处。第一,存储可以不连续:chunk 只要求给出当前这一段,两个缓冲用 chain 拼起来、一个 VecDeque<u8> 都能当一个 Buf 读。第二,解析代码只写一遍,对 &[u8]、Bytes、BytesMut 都能用,而每个类型可以覆盖默认方法、换成更便宜的实现。下面这个解析长度前缀帧的函数对 &[u8] 和 Bytes 各跑一次:

use bytes::{Buf, BufMut, Bytes, BytesMut};

#[path = "../common.rs"]
mod common;
use common::*;

// 长度前缀帧:2 字节大端长度 + 内容。对任何 Buf 都能用
fn parse<B: Buf>(label: &str, mut buf: B) -> Vec<Bytes> {
  let mut out = Vec::with_capacity(4);
  let s = snap();
  while buf.remaining() >= 2 {
    let n = buf.get_u16() as usize; // 读 2 字节,游标前进 2
    out.push(buf.copy_to_bytes(n)); // 取 n 字节,游标前进 n
  }
  let d = delta(s);
  report(label, d, format!("frames={:?}", out));
  out
}

fn main() {
  let mut msg = BytesMut::with_capacity(64);
  msg.put_u16(5);
  msg.put_slice(b"hello");
  msg.put_u16(3);
  msg.put_slice(b"abc");
  let wire = msg.freeze();

  let raw: Vec<u8> = wire.to_vec();
  parse("&[u8]", &raw[..]);
  parse("Bytes", wire.clone());
}

跑 3 次,输出相同:

&[u8]                  alloc+2 (8B) free+0  frames=[b"hello", b"abc"]
Bytes                  alloc+0 (0B) free+0  frames=[b"hello", b"abc"]

同一个 parse,源是 &[u8] 时每帧分配一次、拷贝内容(5 + 3 = 8 字节),源是 Bytes 时一次也不分配。区别在 copy_to_bytes:默认实现新建一个 BytesMut 把字节拷进去再 freeze,而 Bytes 的 Buf 实现把 copy_to_bytes 覆盖成了 split_to:

    fn copy_to_bytes(&mut self, len: usize) -> Self {
        self.split_to(len)
    }

文档还特意说明了 Buf / BufMut 和 std::io::Read / Write 的区别:「A buffer is the value that is provided as an argument to Read::read and Write::write」,Read / Write 可能做系统调用、可能失败,而「Operations on Buf and BufMut are infallible」。所以 get_u16 返回的是 u16 而不是 Result,数据不够时 panic(不想 panic 有 try_get_u16)。三个必需方法和 get_* 的快慢路径在 Buf;chunk_mut 为什么返回 UninitSlice 而不是 &mut [u8],在 BufMut 与 UninitSlice;chain、take 这些组合在 适配器。

动脑筋答案

跑 3 次,输出相同:

cap=65536 Bytes::from  alloc+0 (0B) free+0  
split_to #1            alloc+1 (24B) free+0  
split_to #2            alloc+0 (0B) free+0  same buffer: true
cap=65537 Bytes::from  alloc+1 (24B) free+0  
split_to #1            alloc+0 (0B) free+0  
split_to #2            alloc+0 (0B) free+0  same buffer: true

两种情况总共都是 1 次、24 字节,只是时机不同:

  • 容量等于长度:From<Vec<u8>> 走 into_boxed_slice,Vec 的长度和容量一样,不需要重新分配,Bytes::from 本身 0 次。这时的 Bytes 是可提升存储,没有计数。第一次 split_to 内部要 clone,这才分配那个 24 字节的 Shared,把存储换成共享的;第二次 split_to 只是计数加一。
  • 容量多 1 字节:into_boxed_slice 会把多出的容量缩掉,可能要重新分配并拷贝,所以 From 不走这条路,当场分配 Shared(里面记着真实的 cap,释放时用它),之后的 split_to 都只加计数。
  • 两种情况下第二帧的地址都是原缓冲起点加 656,数据一直留在原来那块里。

小结

  • 网络程序要把一块读缓冲切成很多段,交给不同的所有者,读缓冲本身还要继续用;借用做不到,每段必须是有所有权的值。
  • 64 KiB 切 100 帧:Vec<u8> 100 次分配、拷贝 64 KiB;Arc<[u8]> 加下标 1 次分配,但从 Vec 转过去要整块拷贝;Bytes、BytesMut 只分配一个 24 或 40 字节的 Shared,0 字节拷贝。
  • 区别在计数放在哪里:Arc<[u8]> 的计数和数据在同一块分配里,Bytes 的计数单独一小块,数据留在原地。
  • Bytes 只读、可以廉价地 clone 和切片,背后的存储由手写的 vtable 决定;BytesMut 可写、唯一拥有,split_to 切出互不重叠的段,freeze 不拷贝地转成 Bytes。两者都是 32 字节。
  • Buf / BufMut 把游标从存储里抽出来,必需方法各三个;解析代码写一遍对所有类型都能用,Bytes 把 copy_to_bytes 覆盖成 split_to,不分配。

💬 关联讨论

发起讨论
加载中...