bigywater.tech
Rust · 28

Box<T> 与堆分配

Box 是最简单的智能指针:把一个值放到堆上,拥有它,离开作用域时释放。这篇验证它的三个典型用途:让大对象便宜地移动、让递归类型有限大小、以及它的释放时机,每一条都有真实数字。

E0072
递归类型无限大小,需要 Box 打断
≈ 13,924 倍
移动 Box 相对移动 1 MiB 数组的速度(4 ns vs 61 µs)
8 字节
Box 本身的大小(指针)

Box 是什么

实测 · 本机执行输出
  源码:
      1 | use std::mem::size_of;
      2 | fn main() {
      3 |     let local = 5i32;
      4 |     let boxed = Box::new(5i32);
      5 |     println!("size_of::<i32>() = {},size_of::<Box<i32>>() = {}", size_of::<i32>(), size_of::<Box<i32>>());
      6 |     let stack = &local as *const i32 as usize;
      7 |     let heap = &*boxed as *const i32 as usize;
      8 |     println!("局部 i32 的地址 {:#x}(栈),Box 里的 i32 地址 {:#x}(堆);相差 {} MiB", stack, heap, stack.abs_diff(heap) >> 20);
      9 |     println!("*boxed + 1 = {}(自动解引用)", *boxed + 1);
     10 | }
  编译:成功
  运行(退出码 0):
    size_of::<i32>() = 4,size_of::<Box<i32>>() = 8
    局部 i32 的地址 0x16b82a704(栈),Box 里的 i32 地址 0x104d45300(堆);相差 1642 MiB
    *boxed + 1 = 6(自动解引用)

递归类型

实测 · 本机执行输出
  源码:
      1 | enum List { Cons(i32, List), Nil }
      2 | fn main() {}
  编译:失败
    error[E0072]: recursive type `List` has infinite size
     --> <实验目录>/src/u28b.rs:1:1
      |
    1 | enum List { Cons(i32, List), Nil }
      | ^^^^^^^^^             ---- recursive without indirection
      |
    help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
      |
    1 | enum List { Cons(i32, Box<List>), Nil }
      |                       ++++    +
    
    error: aborting due to 1 previous error
    
    For more information about this error, try `rustc --explain E0072`.
  源码:
      1 | use std::mem::size_of;
      2 | #[allow(dead_code)]
      3 | enum List { Cons(i32, Box<List>), Nil }
      4 | use List::{Cons, Nil};
      5 | fn sum(l: &List) -> i32 { match l { Cons(v, rest) => v + sum(rest), Nil => 0 } }
      6 | fn main() {
      7 |     let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
      8 |     println!("sum = {},size_of::<List>() = {} 字节(i32 + Box 指针 + 标签,与链表长度无关)", sum(&list), size_of::<List>());
      9 | }
  编译:成功
  运行(退出码 0):
    sum = 6,size_of::<List>() = 16 字节(i32 + Box 指针 + 标签,与链表长度无关)

链表等递归结构的更多讨论(为什么 LinkedList 通常不是首选)见LinkedList 为什么通常不是首选。

移动的成本

实测 · 本机执行输出
  源码:
      1 | use std::hint::black_box;
      2 | use std::time::Instant;
      3 | const N: usize = 1 << 20;
      4 | fn pass_array(a: [u8; N]) -> [u8; N] { black_box(a) }
      5 | fn pass_box(a: Box<[u8; N]>) -> Box<[u8; N]> { black_box(a) }
      6 | fn main() {
      7 |     let arr = [1u8; N];
      8 |     let t = Instant::now(); let mut acc = 0u64;
      9 |     for _ in 0..200 { let x = pass_array(black_box(arr)); acc += x[0] as u64; }
     10 |     let t_arr = t.elapsed().as_nanos() as f64 / 200.0;
     11 |     let bx: Box<[u8; N]> = vec![1u8; N].into_boxed_slice().try_into().unwrap();
     12 |     let t = Instant::now(); let mut cur = bx;
     13 |     for _ in 0..200 { cur = pass_box(black_box(cur)); acc += cur[0] as u64; }
     14 |     let t_box = t.elapsed().as_nanos() as f64 / 200.0;
     15 |     black_box(acc);
     16 |     println!("按值传递并返回 [u8; 1 MiB]:每次约 {:.0} ns", t_arr);
     17 |     println!("按值传递并返回 Box<[u8; 1 MiB]>:每次约 {:.1} ns", t_box);
     18 | }
  编译:成功
  运行(退出码 0):
    按值传递并返回 [u8; 1 MiB]:每次约 61267 ns
    按值传递并返回 Box<[u8; 1 MiB]>:每次约 4.4 ns

计时用 release 与 black_box,数字是单次运行结果。内存布局见Box、Rc、Arc 的内存布局。

Box 的释放

实测 · 本机执行输出
  编译:成功
  运行(退出码 0):
    Box 存在时:alloc +1,dealloc +0
    离开作用域后:alloc +1,dealloc +1

分步演示

Box<T>
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / rustc 1.98.1(brew)/ cargo 1.98.1;单个文件用 rustc 直接编译,需要依赖时用 cargo(经代理从 crates.io 下载);计时用 std::time::Instant,数字每次运行会有小幅抖动。计时为 release 构建
    • Box(见 Trait Object 一篇)、Box<[T]> 与 Vec 的转换、栈溢出时 Box 的作用(只在移动成本上验证了 Box,没有验证栈上放不下的大数组的情形)
    京ICP备2026067502号-1京公网安备11010502063487号