切片
主线位置:共享主线上「切」的那一步。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,挪指针
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
以前三个方法遇到空结果都直接 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:可提升存储为什么要先提升
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,但那时只是一次加一、一次减一。
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 才释放(如果它是最后一个句柄的话)。
平台 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 > b | range start must not be greater than end: a <= b | lib.rs 的 range,报在 slice 里 |
slice(..e),e > len | range end out of bounds: e <= len | 同上 |
slice(..=usize::MAX) 等加一溢出 | out of range | 同上 |
split_to(at),at > len | split_to out of bounds: at <= len | split_to |
split_off(at),at > len | split_off out of bounds: at <= len | split_off |
advance(n),n > len | cannot 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。
问: 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才能放开它。