从零写一个最小 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。
问: 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
每格是「分配次数(字节)/ 释放次数」,第一行横线是为了让标准输出的缓冲先分配好。带 * 的行两边不同:
| 步骤 | mini | bytes | 为什么 |
|---|---|---|---|
from(Vec len==cap) 与第一次 clone | 构造时 1 次 | 第一次 clone 时 1 次 | bytes 走可提升存储,计数块等到第一次 clone 才分配(clone 与提升);mini 构造时就分配。两边都只分配一次 |
from(Vec len<cap)、KIND_VEC 的 freeze | 32 字节 | 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 下也说得通。
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>> 总是分配 Shared | len == cap 时转 Box<[u8]>,奇偶两张表,第一次 clone 时 CAS 提升 | 五种存储、clone 与提升 |
| 其他存储 | 只有静态和共享 | from_owner 的 Owned<T>,每个 T 一张表 | 五种存储 |
| 转回去 | 不能 | into_vec、into_mut、is_unique、try_into_mut,唯一时复用缓冲 | clone 与提升 |
| 空切片 | 照常 clone | 结果为空时不 clone,返回地址保留在原处的静态空 Bytes | 切片 |
| 切片的其他形式 | 只有 slice | split_to、split_off、truncate(可提升存储要先提升)、slice_ref | 切片 |
| 扩容 | 没有,写满就 panic | reserve_inner 原地挪回、唯一时扩大、共享时另起新 Vec;原始容量 | reserve |
| 偏移溢出 | 63 位,碰不到 | pos 超过 MAX_VEC_POS 时提升成 Shared | data 字段的编码 |
| 合并 | 没有 | unsplit / try_unsplit,相邻且同一块时 O(1) | split 与 freeze |
| 读写游标 | 两个固有方法 | Buf / BufMut,chunk_mut 交出 UninitSlice | Buf、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 线程的压力测试只说明没观察到泄漏。