Rust in Everything
基于 bytes 1.12.1

切片

主线位置:共享主线上「切」的那一步。hyper 把读到的请求头 freeze 成 Bytes 之后,上层用 slice 取出方法、路径、各个头的值;body 分块交给上层后,解析代码用 split_to / advance 一段段往前吃。这些操作都不拷贝数据:只改 ptr / len,再 clone 一次 data。

flowchart TB
  op["slice / split_to / split_off"] --> chk{越界}
  chk -->|是| panic[panic]
  chk -->|否| empty{结果为空}
  empty -->|是| static["STATIC 空 Bytes,保留地址"]
  empty -->|否| clone["self.clone()"]
  clone --> adjust[改 ptr 和 len]
  clone -.->|可提升存储| promote[第一次 clone 会提升]

Bytes 的字段和 vtable 在 结构与 Vtable,几种存储在 五种存储,clone 时的提升和引用计数在 clone 与提升。这一页讲 bytes.rs 里 impl Bytes 的切片方法,以及它们各自的 panic 信息。BytesMut 的 split_to / split_off 是另一套实现,在 split 与 freeze。

slice:检查范围,clone,挪指针

slice:

    pub fn slice(&self, range: impl RangeBounds<usize>) -> Self {
        let (begin, end) = crate::range(range, self.len());

        if end == begin {
            return Bytes::new_empty_with_ptr(self.ptr.wrapping_add(begin));
        }

        let mut ret = self.clone();

        ret.len = end - begin;
        ret.ptr = unsafe { ret.ptr.add(begin) };

        ret
    }

范围检查在 lib.rs 的 range,注释说它是 core::slice::index::range 的「spiritual copy」,因为后者还不稳定:

#[inline(always)]
#[track_caller]
fn range(range: impl core::ops::RangeBounds<usize>, len: usize) -> (usize, usize) {
    use core::ops::Bound;

    let begin = match range.start_bound() {
        Bound::Included(&n) => n,
        Bound::Excluded(&n) => n.checked_add(1).expect("out of range"),
        Bound::Unbounded => 0,
    };

    let end = match range.end_bound() {
        Bound::Included(&n) => n.checked_add(1).expect("out of range"),
        Bound::Excluded(&n) => n,
        Bound::Unbounded => len,
    };

    assert!(
        begin <= end,
        "range start must not be greater than end: {:?} <= {:?}",
        begin,
        end,
    );
    assert!(
        end <= len,
        "range end out of bounds: {:?} <= {:?}",
        end,
        len,
    );

    (begin, end)
}

它把各种区间写法统一成半开区间 [begin, end)。..=n 和「排除起点」要加一,n 是 usize::MAX 时加不上去,报 out of range。

检查通过、结果非空时,slice 做的事和 clone 一样多,再加两次赋值。对一个刚从 Vec 转来、还没被 clone 过的 Bytes,这次 clone 就是它的第一次 clone,会分配一个 Shared(下面的动脑筋里量过)。对其他存储,代价就是一次原子加一,或者对 'static 数据什么都不做。

空结果:不 clone,但保留地址

结果为空时,slice 不 clone,而是返回 new_empty_with_ptr 造的一个空 Bytes:

    fn new_empty_with_ptr(ptr: *const u8) -> Self {
        debug_assert!(!ptr.is_null());

        // Detach this pointer's provenance from whichever allocation it came from, and reattach it
        // to the provenance of the fake ZST [u8;0] at the same address.
        let ptr = without_provenance(ptr as usize);

        Bytes {
            ptr,
            len: 0,
            data: AtomicPtr::new(ptr::null_mut()),
            vtable: &STATIC_VTABLE,
        }
    }

它挂的是 STATIC_VTABLE:不持有引用计数,clone 和 drop 什么都不做,也不会让原缓冲多活一刻。地址则照原样保留:ptr 是切片起点在原缓冲里的位置。without_provenance 用 null().wrapping_add(addr) 造一个只有地址、不带出处(provenance)的指针,注释说是把它从原分配上「摘下来」,挂到同一地址上一个假想的 [u8; 0] 上。这个空 Bytes 不持有引用计数,原缓冲完全可能先被释放;长度为 0 的切片只需要一个非空、对齐的地址,不需要指向任何活着的分配,所以干脆不让它和原分配有出处上的联系。测试 slice_empty_addr 里有一句注释「Is miri happy about the provenance?」,CI 的 miri 用 -Zmiri-strict-provenance 跑这些测试(ci/miri.sh)。

split_to、split_off 里空结果的分支也用它,所以三个方法的空结果都满足「地址等于对应位置」。只有 slice_ref 例外,见下一节。实测(src/bin/empty_addr.rs,依赖 bytes = "=1.12.1"):

use bytes::Bytes;
use exp_bytes_promote as _; // counting allocator, unused here

fn main() {
  let b = Bytes::from(vec![0u8; 1024]);
  let base = b.as_ptr() as usize;
  let off = |x: &Bytes| (x.as_ptr() as usize).wrapping_sub(base) as isize;
  println!("slice(1024..)        offset = {}", off(&b.slice(1024..)));
  println!("slice(..0)           offset = {}", off(&b.slice(..0)));
  println!("slice(7..7)          offset = {}", off(&b.slice(7..7)));
  println!("split_off(len)       offset = {}", off(&b.clone().split_off(1024)));
  println!("split_to(0)          offset = {}", off(&b.clone().split_to(0)));
  println!("slice_ref(&b[7..7])  same as Bytes::new()? {}", b.slice_ref(&b[7..7]).as_ptr() == Bytes::new().as_ptr());
}

M2 Max、rustc 1.99.0,--release,跑了 3 次,输出相同:

slice(1024..)        offset = 1024
slice(..0)           offset = 0
slice(7..7)          offset = 7
split_off(len)       offset = 1024
split_to(0)          offset = 0
slice_ref(&b[7..7])  same as Bytes::new()? true
📝NOTE
冷知识:空切片的地址是 1.8.0 和 1.11.0 分两次保证下来的

以前三个方法遇到空结果都直接 return Bytes::new(),得到的是一个静态空切片的地址,和原缓冲毫无关系。issue #779(「Preserve addresses of empty bytes in slice」)的作者用 Bytes 写二进制解析器,靠 s.as_ptr() as usize - b.as_ptr() as usize 算当前偏移来报错位置,读到输入末尾时 b.slice(i..) 的地址突然变成了 1,减法溢出 panic。他还指出这与文档矛盾:「This will increment the reference count for the underlying memory and return a new Bytes handle set to the slice.」

split_to / split_off 先一步改好:PR #740(「Guarantee address in split_off/split_to for empty slices」,关联的 issue #738 提议为 Bytes 实现 zerocopy 的 ByteSlice / SplitByteSlice,并说明这些 trait 有安全约定,而那些性质「are not guaranteed by Bytes and BytesMut's public documentation」)加了 new_empty_with_ptr,并在 split_off 的文档里写下「the address of the returned slice is at bytes after that」,进了 1.8.0。slice 则由 PR #780(「Guarantee address in slice() for empty slices.」)把 Bytes::new() 换成 Bytes::new_empty_with_ptr(self.ptr.wrapping_add(begin)),并加了测试 slice_empty_addr,进了 1.11.0。

slice_ref:用地址反推区间

slice_ref 接受一个 &[u8],要求它是 self 的一部分,返回对应的 Bytes。典型用法是:拿 &bytes[..] 交给一个只认 &[u8] 的解析器,解析器返回其中一段,再用 slice_ref 把这一段变回零拷贝的 Bytes。

    pub fn slice_ref(&self, subset: &[u8]) -> Self {
        // Empty slice and empty Bytes may have their pointers reset
        // so explicitly allow empty slice to be a subslice of any slice.
        if subset.is_empty() {
            return Bytes::new();
        }

        let bytes_p = self.as_ptr() as usize;
        let bytes_len = self.len();

        let sub_p = subset.as_ptr() as usize;
        let sub_len = subset.len();

        assert!(
            sub_p >= bytes_p,
            "subset pointer ({:p}) is smaller than self pointer ({:p})",
            subset.as_ptr(),
            self.as_ptr(),
        );
        assert!(
            sub_p + sub_len <= bytes_p + bytes_len,
            "subset is out of bounds: self = ({:p}, {}), subset = ({:p}, {})",
            self.as_ptr(),
            bytes_len,
            subset.as_ptr(),
            sub_len,
        );

        let sub_offset = sub_p - bytes_p;

        self.slice(sub_offset..(sub_offset + sub_len))
    }

判断「是不是子切片」只比地址:起点不能在 self 之前,终点不能在 self 之后。它不知道 subset 是不是真从这块内存借出来的;一个别处的切片只要地址恰好落在范围内,也会被当成子切片,结果是同样内容的零拷贝切片,不会出错。

空的 subset 直接返回 Bytes::new(),不保留地址:注释说空切片和空 Bytes 的指针本来就可能被重置过,所以任何空切片都算任何切片的子切片,不去检查它的地址。这条特判来自 0.5.4,CHANGELOG 里是「Fix panic in Bytes::slice_ref if argument is an empty slice.」

split_to 与 split_off

split_off 让 self 留下 [0, at),返回 [at, len);split_to 反过来,返回 [0, at),self 留下 [at, len):

    pub fn split_to(&mut self, at: usize) -> Self {
        if at == self.len() {
            let end_ptr = self.ptr.wrapping_add(at);
            return mem::replace(self, Bytes::new_empty_with_ptr(end_ptr));
        }

        if at == 0 {
            return Bytes::new_empty_with_ptr(self.ptr);
        }

        assert!(
            at <= self.len(),
            "split_to out of bounds: {:?} <= {:?}",
            at,
            self.len(),
        );

        let mut ret = self.clone();

        // SAFETY: `at` has been asserted to be <= `self.len()`, and the
        // `at == self.len()` and `at == 0` cases were handled above.
        unsafe { self.inc_start(at) };

        ret.len = at;
        ret
    }

两个特殊情况都不 clone:

  • 切出整段(split_to(len)、split_off(0)):用 mem::replace 把 self 原样交出去,self 换成一个空的、地址在对应位置的 Bytes。原来的存储连同它的引用计数一起转移,不加不减。
  • 切出空段(split_to(0)、split_off(len)):返回 new_empty_with_ptr,self 不变。

一般情况下 clone 一次,然后一个改 len,一个用 inc_start 把 ptr 往后挪、len 减少同样的量。两个方法都标了 #[must_use],提示分别是「consider Bytes::advance if you don't need the other half」和「consider Bytes::truncate if you don't need the other half」:只想丢掉一半时,advance / truncate 不需要 clone。Buf 的 advance 只调 inc_start;copy_to_bytes 就是 split_to(Buf 那边的约定见 Buf)。

越界断言放在两个特殊情况之后,是 1.7.0 的调整(CHANGELOG:「Reorder assertion in Bytes::split_to, Bytes::split_off (#689, #693)」)。PR #689 的理由是:「If at == self.len() then we already know at <= self.len(). If at == 0, it can't be greater than self.len().」

truncate 与 clear:可提升存储为什么要先提升

truncate:

    pub fn truncate(&mut self, len: usize) {
        if len < self.len {
            // The Vec "promotable" vtables do not store the capacity,
            // so we cannot truncate while using this repr. We *have* to
            // promote using `split_off` so the capacity can be stored.
            if self.vtable as *const Vtable == &PROMOTABLE_EVEN_VTABLE
                || self.vtable as *const Vtable == &PROMOTABLE_ODD_VTABLE
            {
                drop(self.split_off(len));
            } else {
                self.len = len;
            }
        }
    }

对大多数存储,截短就是 self.len = len。可提升存储不行,原因在它释放内存的方式。可提升存储不存容量,drop 时用 free_boxed_slice 现算:

unsafe fn free_boxed_slice(buf: *mut u8, offset: *const u8, len: usize) {
    let cap = offset.offset_from(buf) as usize + len;
    dealloc(buf, Layout::from_size_align(cap, 1).unwrap())
}

cap = (ptr - buf) + len 只在「ptr + len 是原分配的末尾」时成立。advance / split_to 把 ptr 往后挪、len 等量减少,和不变;直接改小 len 就会让算出来的 cap 变小,dealloc 拿到的 Layout 和分配时的不一样,违反 GlobalAlloc::dealloc 的约定。into_vec、into_mut 重建 Vec 时用的也是同一个算式。

所以 truncate 改用 split_off(len):它内部的 clone 会把存储提升成 Shared,真实的 cap 在提升那一刻被记进 Shared;然后把切下来的后半段 drop 掉,引用计数回到 1。代价是一次 Shared 的分配,下面的动脑筋量了这一点。提升之后 self 的 vtable 仍然是 PROMOTABLE_*,以后再 truncate 还会走 split_off,但那时只是一次加一、一次减一。

📝NOTE
冷知识:truncate 曾经在可提升存储上算错容量

issue #360(「Bytes may recreate Vec incorrectly on drop」)的复现只有三行:Bytes::from(vec![10, 20, 30]),truncate(2),drop。作者在源码里加了打印,看到「dismantled a vec of cap 3」「rebuilding vec with cap 2」,并指出这违反了 dealloc 的规定:「layout must be the same layout that was used to allocate that block of memory」。他用 jemalloc 循环跑 Bytes::from(vec![10; 100000000]) + truncate(1),「After a few seconds of run, macOS Activity Monitor shows 100Gb of memory usage」。

那时的 truncate 只有一行 self.len = len;。PR #361(「Fix Bytes::truncate losing the original Vec's capacity」)改成了上面这段判断 vtable、走 split_off 的代码,并新增 tests/test_bytes_vec_alloc.rs:一个记下每次分配大小、释放时大小不符就 panic 的全局分配器,测试 advance、truncate 及两者组合。CHANGELOG 记在 0.5.4:「Fix Bytes::truncate losing original capacity when repr is an unshared Vec.」

clear 就是 self.truncate(0)。对可提升存储,它走 split_off(0),也就是上一节的「切出整段」分支:mem::replace 把整个存储交出来再 drop,self 变成空的静态 Bytes。不提升、不分配,缓冲当场释放。对其他存储,clear 只把 len 置 0,引用计数不变,缓冲要等这个 Bytes 被 drop 才释放(如果它是最后一个句柄的话)。

❗IMPORTANT
动脑筋:同一个操作,对两种存储各分配、释放几次?

平台 M2 Max、rustc 1.99.0,--release。promotable 是 Bytes::from(vec![0u8; 1024])(len == cap,可提升存储,从没 clone 过);shared 是从 len = 1024、cap = 2048 的 Vec 转来的(From<Vec<u8>> 直接建 Shared)。count 用的是 clone 与提升 里那个计数分配器,只数闭包内部的分配和释放;返回的那一半在计数之后才 drop。

use bytes::Bytes;
use exp_bytes_promote::count;

fn main() {
  // allocation cost of each op on a fresh promotable Bytes vs a Shared one
  type Op = fn(&mut Bytes) -> Option<Bytes>;
  let ops: [(&str, Op); 7] = [
    ("slice(10..20)", |b| Some(b.slice(10..20))),
    ("slice(5..5)", |b| Some(b.slice(5..5))),
    ("split_to(10)", |b| Some(b.split_to(10))),
    ("split_off(10)", |b| Some(b.split_off(10))),
    ("split_off(0)", |b| Some(b.split_off(0))),
    ("truncate(10)", |b| { b.truncate(10); None }),
    ("clear()", |b| { b.clear(); None }),
  ];
  for (name, op) in ops {
    let mut promotable = Bytes::from(vec![0u8; 1024]);
    let (r1, a1, d1) = count(|| op(&mut promotable));
    let mut v = Vec::with_capacity(2048);
    v.resize(1024, 0u8);
    let mut shared = Bytes::from(v);
    let (r2, a2, d2) = count(|| op(&mut shared));
    println!(
      "{name:<14} promotable: allocs={a1} deallocs={d1} (self.len={:>4})   shared: allocs={a2} deallocs={d2}",
      promotable.len()
    );
    drop((r1, r2));
  }
}

重点猜两行:truncate(10) 和 clear() 在 promotable 一列各是多少?

越界时的 panic 信息

把每种越界都跑一遍,用 panic hook 打出 panic 位置(截掉 cargo 注册表的路径前缀)和信息原文。BACKING 是一段更长的静态数据,b 只取其中 11 字节,这样 slice_ref 的两个越界参数都是合法的切片:

use std::ops::Bound;
use std::panic;

use bytes::{Buf, Bytes};

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());
  }));

  static BACKING: &[u8] = b"say hello world, again";
  let b = Bytes::from_static(&BACKING[4..15]); // "hello world", len 11
  let cases: Vec<(&str, Box<dyn Fn()>)> = vec![
    ("b.slice(5..3)", Box::new(|| drop(b.slice(5..3)))),
    ("b.slice(..20)", Box::new(|| drop(b.slice(..20)))),
    ("b.slice(..=usize::MAX)", Box::new(|| drop(b.slice(..=usize::MAX)))),
    ("b.slice((Excluded(usize::MAX), Unbounded))", Box::new(|| drop(b.slice((Bound::Excluded(usize::MAX), Bound::Unbounded))))),
    ("b.clone().split_to(20)", Box::new(|| drop(b.clone().split_to(20)))),
    ("b.clone().split_off(20)", Box::new(|| drop(b.clone().split_off(20)))),
    ("b.clone().advance(20)", Box::new(|| b.clone().advance(20))),
    ("b.slice_ref(&BACKING[0..3])", Box::new(|| drop(b.slice_ref(&BACKING[0..3])))),
    ("b.slice_ref(&BACKING[10..17])", Box::new(|| drop(b.slice_ref(&BACKING[10..17])))),
  ];
  for (name, f) in cases {
    println!("{name}");
    let _ = panic::catch_unwind(panic::AssertUnwindSafe(f));
  }
  let mut t = b.clone();
  t.truncate(20);
  println!("b.clone().truncate(20): no panic, len = {}", t.len());
}

M2 Max、rustc 1.99.0,--release,跑了 3 次,除 slice_ref 两行里的地址外输出相同:

b.slice(5..3)
    panicked at src/bytes.rs:374: range start must not be greater than end: 5 <= 3
b.slice(..20)
    panicked at src/bytes.rs:374: range end out of bounds: 20 <= 11
b.slice(..=usize::MAX)
    panicked at src/bytes.rs:374: out of range
b.slice((Excluded(usize::MAX), Unbounded))
    panicked at src/bytes.rs:374: out of range
b.clone().split_to(20)
    panicked at src/bytes.rs:533: split_to out of bounds: 20 <= 11
b.clone().split_off(20)
    panicked at src/bytes.rs:481: split_off out of bounds: 20 <= 11
b.clone().advance(20)
    panicked at src/bytes.rs:694: cannot advance past `remaining`: 20 <= 11
b.slice_ref(&BACKING[0..3])
    panicked at src/bytes.rs:426: subset pointer (0x104a49e3d) is smaller than self pointer (0x104a49e41)
b.slice_ref(&BACKING[10..17])
    panicked at src/bytes.rs:432: subset is out of bounds: self = (0x104a49e41, 11), subset = (0x104a49e47, 7)
b.clone().truncate(20): no panic, len = 11
调用信息位置
slice(a..b),a > brange start must not be greater than end: a <= blib.rs 的 range,报在 slice 里
slice(..e),e > lenrange end out of bounds: e <= len同上
slice(..=usize::MAX) 等加一溢出out of range同上
split_to(at),at > lensplit_to out of bounds: at <= lensplit_to
split_off(at),at > lensplit_off out of bounds: at <= lensplit_off
advance(n),n > lencannot advance past `remaining`: n <= len<Bytes as Buf>::advance
slice_ref,起点在前subset pointer (…) is smaller than self pointer (…)slice_ref
slice_ref,终点在后subset is out of bounds: self = (…, len), subset = (…, len)slice_ref
truncate(n),n > len不 panic,什么也不做—

range 标了 #[track_caller],所以前四个 panic 报的位置是 slice 里调用 range 的那一行(bytes.rs:374),而不是 lib.rs。slice 本身没有 #[track_caller],panic 位置不会落到用户代码上;要知道是哪次调用越界,得看 backtrace。

📝NOTE
没有蠢问题

问: range end out of bounds: 20 <= 11,20 明明不小于等于 11,这句话是不是写反了?

答: 没写反,只是写法省事:冒号后面是被断言的条件,填上了实际的值。assert!(end <= len, "...: {:?} <= {:?}", end, len) 读作「要求 end <= len,实际是 20 和 11」。split_to、split_off、advance 的信息都是同一个格式,range start must not be greater than end: 5 <= 3 也一样:要求 begin <= end,实际 begin 是 5、end 是 3。

一个小切片留住整块缓冲

切片和原 Bytes 共享同一个 Shared,引用计数只记「有几个句柄」,不记「每个句柄看多大一段」。只要还有一个句柄活着,整块分配就不会释放,哪怕这个句柄只看 10 字节。用计数分配器的 LIVE(当前存活的字节数)看一遍,src/bin/retain.rs:

use std::sync::atomic::Ordering::Relaxed;

use bytes::Bytes;
use exp_bytes_promote::LIVE;

fn live(step: &str, base: usize) {
  println!("{step:<40} live = {:>8} bytes", LIVE.load(Relaxed) - base);
}

fn main() {
  println!("(stdout warmed up before measuring)");
  let base = LIVE.load(Relaxed);
  let big = Bytes::from(vec![b'x'; 1 << 20]);
  live("big = 1 MiB Bytes", base);
  let small = big.slice(100..110);
  live("small = big.slice(100..110)", base);
  drop(big);
  live("drop(big)", base);
  drop(small);
  live("drop(small)", base);

  println!();
  let big = Bytes::from(vec![b'x'; 1 << 20]);
  live("big = 1 MiB Bytes", base);
  let small = Bytes::copy_from_slice(&big[100..110]);
  live("small = copy_from_slice(&big[100..110])", base);
  drop(big);
  live("drop(big)", base);
  drop(small);
  live("drop(small)", base);
}

第一行 println! 是为了让标准输出的缓冲先分配好,不混进测量。M2 Max、rustc 1.99.0,--release,跑了 3 次,输出相同:

(stdout warmed up before measuring)
big = 1 MiB Bytes                        live =  1048576 bytes
small = big.slice(100..110)              live =  1048600 bytes
drop(big)                                live =  1048600 bytes
drop(small)                              live =        0 bytes

big = 1 MiB Bytes                        live =  1048576 bytes
small = copy_from_slice(&big[100..110])  live =  1048586 bytes
drop(big)                                live =       10 bytes
drop(small)                              live =        0 bytes
  • slice(100..110) 多出 24 字节:第一次 clone 提升出来的 Shared。
  • drop(big) 之后仍是 1 048 600 字节:1 MiB 缓冲和 Shared 都还在,因为 small 还持有一份引用计数。直到 drop(small) 才一起释放。
  • 换成 Bytes::copy_from_slice:多出 10 字节的独立分配,drop(big) 立刻把 1 MiB 还回去。

这就是「切片共享整块分配」的代价。长期保存一小段数据(缓存里的键、从大响应里挑出来的一个字段)时,用 copy_from_slice 拷一份,比留着切片划算。常见坑一章会从这个实验出发讲实际场景。

动脑筋答案

slice(10..20)  promotable: allocs=1 deallocs=0 (self.len=1024)   shared: allocs=0 deallocs=0
slice(5..5)    promotable: allocs=0 deallocs=0 (self.len=1024)   shared: allocs=0 deallocs=0
split_to(10)   promotable: allocs=1 deallocs=0 (self.len=1014)   shared: allocs=0 deallocs=0
split_off(10)  promotable: allocs=1 deallocs=0 (self.len=  10)   shared: allocs=0 deallocs=0
split_off(0)   promotable: allocs=0 deallocs=0 (self.len=   0)   shared: allocs=0 deallocs=0
truncate(10)   promotable: allocs=1 deallocs=0 (self.len=  10)   shared: allocs=0 deallocs=0
clear()        promotable: allocs=0 deallocs=1 (self.len=   0)   shared: allocs=0 deallocs=0

跑了 3 次,输出相同。truncate(10) 是分配 1 次、释放 0 次;clear() 是分配 0 次、释放 1 次。

  • slice(10..20)、split_to(10)、split_off(10):在从没 clone 过的可提升存储上都是第一次 clone,各分配一个 Shared;shared 一列只是引用计数加一。
  • slice(5..5):空结果走 new_empty_with_ptr,不 clone,所以连可提升存储也不提升。
  • split_off(0):「切出整段」,mem::replace 交出原存储,不 clone、不分配。
  • truncate(10):可提升存储走 split_off(10),提升分配 1 次 Shared;切下来的后半段在闭包里就 drop 了,但只是把引用计数从 2 减回 1,不释放。shared 只改 len。
  • clear():可提升存储走 split_off(0),整段交出后当场 drop,那 1 次释放就是 1024 字节的缓冲。shared 一列只把 len 置 0,什么都没释放:2048 字节的缓冲要等这个 Bytes 本身 drop。

小结

  • slice、split_to、split_off 的一般路径都是「检查范围 → clone → 改 ptr / len」。在没 clone 过的可提升存储上,这次 clone 会分配一个 Shared。
  • 结果为空时不 clone,返回挂 STATIC_VTABLE、地址保留在对应位置的空 Bytes(split_* 自 1.8.0,slice 自 1.11.0);slice_ref 遇到空参数仍返回 Bytes::new()。
  • split_to(len) / split_off(0) 用 mem::replace 交出整个存储,不 clone。
  • slice_ref 只按地址判断子切片:起点不早于、终点不晚于 self。
  • 可提升存储不存容量,靠「ptr + len 是分配末尾」算 cap;所以 truncate 要先经 split_off 提升(0.5.4 修的 bug),而 clear 在可提升存储上会当场释放缓冲,在其他存储上只把 len 置 0。
  • 越界 panic 的信息格式是「被断言的条件: 实际值」,如 range end out of bounds: 20 <= 11、split_to out of bounds: 20 <= 11。
  • 切片共享整块分配:10 字节的切片能让 1 MiB 缓冲一直活着,Bytes::copy_from_slice 才能放开它。

💬 关联讨论

发起讨论
加载中...