原文
https://exyr.org/2018/rust-arenas-vs-dropck/
内容
所有权与借用是rust数据结构的的基础。然而,只有在创造变量之后才能取得其所有权(移动它)或引用它。 这种方式似乎可以防止在数据结构中出现循环(引用),即使(循环引用)有时很有必要。 例如,在网页内容树中,从任何DOM节点,都可以轻松访问(如果有的话)其第一个和最后一个子节点,前一个和下一个兄弟节点(以便节点的子节点构成双向链接列表)和父节点。 其他应用程序也需要类似的通用操纵图。
有以下几种方式来处理这种局限性:
- 引用计数器,
Rc
和Arc
- 建立共享内存索引
&T
,用arena allocator
Arena Allocation
Arena Allocation是一种非传统的内存管理方法。它通过顺序化分配内存,内存数据分块等特性使内存碎片粗化,有效改善了内存碎片导致的Full GC问题。
它的原理:
创建一个大小固定的bytes数组和一个偏移量,默认值为0。
分配对象时,将新对象的data bytes复制到数组中,数组的起始位置是偏移量,复制完成后为偏移量自增data.length的长度,这样做是防止下次复制数据时不会覆盖掉老数据(append)。
当一个数组被充满时,创建一个新的数组。
清理时,只需要释放掉这些数组,即可得到固定的大块连续内存。
在Arena Allocation方案中,数组的大小影响空间连续性,越大内存连续性越好,但内存平均利用率会降低。
构建&T
循环引用
在编译器处理自引用时,我们需要引用自己的类型。 一下天真的尝试会产生一种会占用无限空间的类型,编译器不支持。
let mut a = (42_u32, None);
let b = (7_u32, Some(&a));
// error[E0308]: mismatched types
a.1 = Some(b);
// ^^^^^^^ cyclic type of infinite size
我们需要创建一种可以循环的类型,如下:
struct Node<'a> {
value: u32,
next: Option<&'a Node<'a>>,
}
let mut a = Node { value: 42, next: None };
// error[E0506]: cannot assign to `a.next` because it is borrowed
let b = Node { value: 7, next: Some(&a) };
// - borrow of `a.next` occurs here
a.next = Some(&b);
// ^^^^^^^^^^^^^^ assignment to borrowed `a.next` occurs here
发生了一次borrow-checking
错误,
=
与&mut T
一样具有排他性,( 从功能上来说,&mut T
和&T
,应该叫做排他引用和共享引用,而不是可变与不可变 )。
use std::cell::Cell;
struct Node<'a> {
value: u32,
next: Cell<Option<&'a Node<'a>>>,
}
let a = Node { value: 42, next: Cell::new(None) };
let b = Node { value: 7, next: Cell::new(Some(&a)) };
// error[E0597]: `b` does not live long enough
a.next.set(Some(&b));
// ^ borrowed value does not live long enough
// `b` dropped here while still borrowed
现在我们触摸到了问题的核心,上述代码中的a
和b
需要一样的生命周期。
我们写出&'a Node<'a>
,暗示了外部引用的生命周期与内部节点一致。如果我们写出&'a Node<'b>
,Node
则需要两个生命周期参数。
next
需要更新为&'a Node<'b, 'b>
,如果写出&’a Node<’b, ‘c>,我们需要三个生命周期参数,我们又回到了无限大的类型。
一种使得节点有着相同生命周期的方法是把节点放入同一个复合类型中,如tuple:
use std::cell::Cell;
struct Node<'a> { value: u32, next: Cell<Option<&'a Node<'a>>> }
let (a, b, c) = (
Node { value: 0, next: Cell::new(None) },
Node { value: 7, next: Cell::new(None) },
Node { value: 42, next: Cell::new(None) },
);//a, b, c,处于同一个元组,生命周期相同。
// Create a cycle between b and c:
a.next.set(Some(&b));
b.next.set(Some(&c));
c.next.set(Some(&b));
// Traverse the graph just to show it works:
let mut node = &a;
let mut values = Vec::new();
for _ in 0..10 {
values.push(node.value);
node = node.next.get().unwrap()
}
assert_eq!(values, [0, 7, 42, 7, 42, 7, 42, 7, 42, 7])
上述的代码并不实用,因为需要提前获知节点的数量,并一次性创建它们。
用Vec
和 RefCell
创建一个简单的arena allocator
我们的目标是在共享存储上动态创建一些节点,并当存储被放弃时,销毁节点。这种方式在Rust中已经出现了叫arena allocator
。它需要用到
unsafe
,但Rust的借用机制可以提供安全的API(通过把它封装在一个模块中,别的代码不会扰乱他)。
如下代码是一个简单的实现:
use std::cell::RefCell;
pub struct Arena<T> {
chunks: RefCell<Vec<Vec<T>>>,
}
impl<T> Arena<T> {
pub fn new() -> Arena<T> {
Arena {
chunks: RefCell::new(vec![Vec::with_capacity(8)]),
}
}
pub fn allocate(&self, value: T) -> &T {
let mut chunks = self.chunks.borrow_mut();
if chunks.last().unwrap().len() >= chunks.last().unwrap().capacity() {
let new_capacity = chunks.last().unwrap().capacity() * 2;
chunks.push(Vec::with_capacity(new_capacity))
}
chunks.last_mut().unwrap().push(value);
let value_ptr: *const T = chunks.last().unwrap().last().unwrap();
unsafe {
// Unsafely dereference a raw pointer to artificially
// extend the lifetime of the returned reference
&*value_ptr
}
}
}
/
use std::cell::Cell;
struct Node<'arena> { value: u32, next: Cell<Option<&'arena Node<'arena>>> }
// impl<'arena> Drop for Node<'arena> { fn drop(&mut self) {} }
let arena = Arena::new();
let c = arena.allocate(Node { value: 42, next: Cell::new(None) });
let b = arena.allocate(Node { value: 7, next: Cell::new(Some(c)) });
let a = arena.allocate(Node { value: 0, next: Cell::new(Some(b)) });
c.next.set(Some(b));
let mut node = a;
let mut values = Vec::new();
for _ in 0..10 {
values.push(node.value);
node = node.next.get().unwrap()
}
assert_eq!(values, [0, 7, 42, 7, 42, 7, 42, 7, 42, 7])
上述代码的关键点在于把节点T
放入Vec<T>
,同时小心不要超过初始容量。当超过容量便创建一个新的,并保存,已分配的Vec
不必重新分配,unsafe
只有很小一块,它的稳固性基于不会对已经分配的项进行改变。