rust arena 内存分配

6 阅读4分钟

在 Rust 中,Arena 分配器是一种特殊的内存分配模式,它会在一个连续的内存区域(称为 Arena)中分配对象,并一次性释放所有对象,而不是单独释放每个对象。这种模式在某些场景下非常高效,比如解析器、编译器中间表示、游戏实体管理等。


1. Arena 的核心特点

  • 批量分配,批量释放:所有分配的对象在 Arena 生命周期结束时一起释放。
  • 避免碎片化:对象在 Arena 中连续分配,内存布局紧凑。
  • 高性能:分配操作通常只是移动指针,释放操作是 O(1) 的。
  • 所有权集中:Arena 拥有其中所有对象的所有权,对象之间可以安全地相互引用。

2. 常见使用场景

  • AST(抽象语法树)节点:在编译器/解释器中,AST 节点在解析阶段分配,解析结束后一次性释放。
  • 游戏实体:一帧中创建的所有游戏对象在帧结束时批量释放。
  • 复杂数据结构:如链表、图等,其中节点需要相互引用且生命周期一致。

3. Rust 中的 Arena 实现

Rust 生态中有几个成熟的 Arena 库:

typed-arena

允许分配单一类型的对象。

use typed_arena::Arena;

struct Node {
    value: i32,
    children: Vec<&'static Node>, // 注意生命周期标记
}

fn main() {
    let arena = Arena::new();
    
    let child1 = arena.alloc(Node { value: 1, children: vec![] });
    let child2 = arena.alloc(Node { value: 2, children: vec![] });
    let root = arena.alloc(Node {
        value: 0,
        children: vec![child1, child2],
    });
    
    // 所有节点由 arena 统一管理,生命周期相同
    // arena 超出作用域时,所有节点一起释放
}

bumpalo

通用的 bump allocator,支持多类型分配。

use bumpalo::Bump;

fn main() {
    let bump = Bump::new();
    
    let i = bump.alloc(42);
    let s = bump.alloc_str("hello");
    let vec = bump.alloc(vec![1, 2, 3]);
    
    // 所有分配的内存由 bump 统一管理
}

自实现简单 Arena

use std::cell::RefCell;
use std::mem;

struct SimpleArena {
    chunks: RefCell<Vec<Vec<u8>>>,
    current: RefCell<Vec<u8>>,
    pos: RefCell<usize>,
}

impl SimpleArena {
    fn new() -> Self {
        Self {
            chunks: RefCell::new(Vec::new()),
            current: RefCell::new(Vec::with_capacity(1024)),
            pos: RefCell::new(0),
        }
    }
    
    fn allocate<T>(&self, value: T) -> &mut T {
        let size = mem::size_of::<T>();
        let align = mem::align_of::<T>();
        
        let mut pos = self.pos.borrow_mut();
        let mut current = self.current.borrow_mut();
        
        // 对齐调整
        let aligned_pos = (*pos + align - 1) & !(align - 1);
        
        // 检查当前 chunk 是否有足够空间
        if aligned_pos + size > current.capacity() {
            let new_chunk = Vec::with_capacity(1024.max(size * 2));
            self.chunks.borrow_mut().push(mem::replace(&mut *current, new_chunk));
            *pos = 0;
            return self.allocate(value); // 递归重试
        }
        
        // 确保有足够容量
        if aligned_pos + size > current.len() {
            current.resize(aligned_pos + size, 0);
        }
        
        // 写入数据
        let ptr = current.as_mut_ptr().add(aligned_pos);
        unsafe {
            ptr.copy_from_nonoverlapping(&value as *const T as *const u8, size);
            mem::forget(value); // 防止原值被 drop
            *pos = aligned_pos + size;
            &mut *(ptr as *mut T)
        }
    }
}

// 使用示例
fn main() {
    let arena = SimpleArena::new();
    let x = arena.allocate(42_i32);
    let y = arena.allocate(String::from("hello"));
    
    println!("{} {}", x, y);
    // Arena 销毁时,所有内存一次性释放
}

4. 生命周期处理

Arena 分配的对象通常需要与 Arena 本身具有相同的生命周期:

use typed_arena::Arena;

struct Graph<'a> {
    nodes: Arena<Node<'a>>,
}

struct Node<'a> {
    edges: Vec<&'a Node<'a>>, // 可以安全引用同一 arena 中的其他节点
}

5. 性能注意事项

  • 分配速度:Arena 分配通常只是指针移动,比全局分配器快得多。
  • 内存使用:可能浪费空间(对齐间隙、最后 chunk 未用完的空间)。
  • 释放时机:只能一次性释放所有对象,不能单独释放。
  • 缓存友好:连续分配的对象在内存中相邻,缓存局部性好。

6. 与标准分配器的对比

特性Arena标准分配器(如 jemalloc)
分配速度极快(指针移动)较慢(系统调用/复杂管理)
释放速度O(1)O(n) 或更复杂
内存碎片无内部碎片可能有碎片
灵活性只能批量释放可单独释放
适用场景同生命周期对象组通用场景

7. 实际应用建议

  1. 使用现成的库(如 typed-arenabumpalo)而非自己实现。
  2. 明确对象是否具有相同的生命周期。
  3. 注意 Arena 分配的对象不能比 Arena 本身活得久。
  4. 对于需要析构的对象,Arena 会正确调用 drop(但释放内存是批量的)。

8. 高级模式:两阶段 Arena

// 第一阶段:分配所有节点
// 第二阶段:处理节点间引用
// 第三阶段:一次性释放

Arena 在 Rust 中是处理特定内存管理模式的强大工具,尤其适合编译器、解析器等需要高效分配大量同生命周期对象的场景。