Box: 在堆上分配内存
从c/c++得到Box灵感
我们先看 Box
为什么有 Box
-
C 需要使用 malloc/calloc/realloc/free 来处理内存的分配,很多时候,被分配出来的内存 在函数调用中来来回回使用,导致谁应该负责释放这件事情很难确定,给开发者造成了极 大的心智负担。
-
C++ 在此基础上改进了一下,提供了一个智能指针 unique_ptr,可以在指针退出作用 域的时候释放堆内存,这样保证了堆内存的单一所有权。这个 unique_ptr 就是 Rust 的 Box
的前身。
Box
#![allow(unused)] fn main() { pub struct Unique<T: ?Sized> { pointer: *const T, // NOTE: this marker has no consequences for variance, but is necessary // for dropck to understand that we logically own a `T`. // // For details, see: // https://github.com/rust-lang/rfcs/blob/master/text/0769-sound-generic-drop.md#phantom-data _marker: PhantomData<T>, } }
堆上分配内存的 Box 其实有一个缺省的泛型参数 A
设计内存分配器的目的除了保证正确性之外,就是为了有效地利用剩余内存,并控制内存 在分配和释放过程中产生的碎片的数量。
在多核环境下,它还要能够高效地处理并发请 求。(如果你对通用内存分配器感兴趣,可以看参考资料) 堆上分配内存的 Box
其实有一个缺省的泛型参数 A,就需要满足 Allocator trait, 并且默认是 Global:
#![allow(unused)] fn main() { pub struct Box<T: ?Sized, A: Allocator = Global>(Unique<T>, A); }
Allocator trait 提供很多方法:
-
allocate 是主要方法,用于分配内存,对应 C 的 malloc/calloc;
-
deallocate,用于释放内存,对应 C 的 free;
-
还有 grow / shrink,用来扩大或缩小堆上已分配的内存,对应 C 的 realloc。
替换默认的内存分配器
如果你想替换默认的内存分配器,可以使用 #[global_allocator] 标记宏,定义你自己的全局分配器。下面的代码展示了如何在 Rust 下使用jemalloc:
use jemallocator::Jemalloc; #[global_allocator] static GLOBAL: Jemalloc = Jemalloc; fn main() {}
实现内存分配器
内存分配器
use std::alloc::{GlobalAlloc, Layout, System}; struct MyAllocator; unsafe impl GlobalAlloc for MyAllocator { unsafe fn alloc(&self, layout: Layout) -> *mut u8 { let data = System.alloc(layout); eprintln!("ALLOC: {:p}, size {}", data, layout.size()); data } unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { System.dealloc(ptr, layout); eprintln!("FREE: {:p}, size {}", ptr, layout.size()); } } #[global_allocator] static GLOBAL: MyAllocator = MyAllocator; #[allow(dead_code)] struct Matrix { // 使用不规则的数字如 505 可以让 dbg! 的打印很容易分辨出来 data: [u8; 505], } impl Default for Matrix { fn default() -> Self { Self { data: [0; 505] } } } fn main() { // 在这句执行之前已经有一些内存分配和释放 let data = Box::new(Matrix::default()); println!( "!!! allocated memory: {:p}, len: {}", &*data, std::mem::size_of::<Matrix>() ); // data 在这里 drop,可以在打印中看到 FREE // 之后还有很多其它内存被释放 }
- 这里 MyAllocator 就用 System allocator,然后加 eprintln!(),和我 们常用的 println!() 不同的是,eprintln!() 将数据打印到 stderr
- 注意这里不能使用 println!() 。因为 stdout 会打印到一个由 Mutex 互斥锁保护的共享全 局 buffer 中,这个过程中会涉及内存的分配,分配的内存又会触发 println!(),最终造成 程序崩溃。而 eprintln! 直接打印到 stderr,不会 buffer。
- 在使用 Box 分配堆内存的时候要注意,Box::new() 是一个函数,所以传入它的数据会出现 在栈上,再移动到堆上。所以,如果我们的 Matrix 结构不是 505 个字节,是一个非常大 的结构,就有可能出问题。
// #![feature(dropck_eyepatch)] // struct MyBox<T>(Box<T>); // unsafe impl<#[may_dangle] T> Drop for MyBox<T> { // fn drop(&mut self) { // todo!(); // } // } fn main() { // 在堆上分配 16M 内存,但它会现在栈上出现,再移动到堆上 let boxed = Box::new([0u8; 1 << 24]); println!("len: {}", boxed.len()); }
cargo run --bin box
或者在 playground 里运行,直接栈溢出 stack overflow- 本地使用 “cargo run –bin box —release” 编译成 release 代码运行,会正常执行!
这是因为 “cargo run” 或者在 playground 下运行,默认是 debug build,它不会做任 何 inline 的优化,而 Box::new() 的实现就一行代码,并注明了要 inline,在 release 模式 下,这个函数调用会被优化掉, 本质是编译器自动调用下列方式:
#![allow(unused)] fn main() { #[cfg(not(no_global_oom_handling))] #[inline(always)] #[doc(alias = "alloc")] #[doc(alias = "malloc")] #[stable(feature = "rust1", since = "1.0.0")] pub fn new(x: T) -> Self { box x } }
这里的关键字 box是 Rust 内部的关键字,用户代码无法调用,它只出现在 Rust 代码中,用于分配堆内存,box 关键字在编译时,会使用内存分配器 分配内存。
内存如何释放
Box默认实现的Drop trait
#![allow(unused)] fn main() { #[stable(feature = "rust1", since = "1.0.0")] unsafe impl<#[may_dangle] T: ?Sized, A: Allocator> Drop for Box<T, A> { fn drop(&mut self) { // FIXME: Do nothing, drop is currently performed by compiler. } } }