2026年分配器的现状 - 六个月后
摘要
本文更新了Rust中自定义分配器的稳定化进展,重点介绍了最近的开发,例如Allocator trait变得dyn兼容,以及解决不健全性问题的努力。
<p><a href="https://lobste.rs/s/9tck4q/state_allocators_2026_6_months_later">评论</a></p>
查看缓存全文
缓存时间: 2026/09/09 09:15
# 2026年分配器现状
来源:https://cetra3.github.io/blog/state-of-allocators-2026-part-2/
## 2026年分配器现状:六个月后进展
*我们六个月后的进展与未来展望*
rust2026-09-09
自上一篇文章(https://cetra3.github.io/blog/state-of-allocators-2026)发布至今,我很高兴地报告:Rust自定义分配器的稳定化工作重焕活力,我们正处在前所未有的接近成功时刻(https://github.com/rust-lang/rust/pull/156882)。需要说明的是,虽然我通过线上线下的方式报道并推动此事,但我并非主要驱动力。Nia在领导这项工作方面表现卓越,大部分赞誉应当归功于她和libs团队。
那么,目前自定义分配器的状况如何?下一步又将如何?
## 分配器形态
根据稳定的分配器trait PR(https://github.com/rust-lang/rust/pull/156882)及后续稳定化报告(https://hackmd.io/nNHdKkp1TTK7jat0I-ABqA),当前分配器形态与nightly版本相比并无*太大*差异。初始API范围相当有限(原因后述),但已具备足够的基础可供构建。核心`Allocator`trait仅包含两个方法(其他方法另有提供):
```rust
unsafe trait Allocator {
fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError>;
unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout);
}
```
其他相关接口层涉及与`Vec`和`Box`的协同使用。例如`Box::new_in`:
```rust
impl<T, A: Allocator> Box<T, A> {
fn new_in(x: T, alloc: A) -> Box<T, A>;
}
```
以及`Vec::new_in`和相关方法:
```rust
impl<T, A: Allocator> Vec<T, A> {
// 安全方法
fn new_in(alloc: A) -> Vec<T, A>;
fn with_capacity_in(capacity: usize, alloc: A) -> Self;
fn allocator(&self) -> &A;
// 解构方法
unsafe fn from_raw_parts_in(
ptr: *mut T,
length: usize,
capacity: usize,
alloc: A,
) -> Self;
unsafe fn from_parts_in(
ptr: NonNull<T>,
length: usize,
capacity: usize,
alloc: A,
) -> Self;
fn into_raw_parts_with_allocator(self) -> (*mut T, usize, usize, A);
fn into_parts_with_allocator(self) -> (NonNull<T>, usize, usize, A);
}
```
### `dyn Allocator`兼容性
稳定化期间的关键问题之一是:`Allocator`是否应被设计为`dyn`兼容。由于API范围精简,且能解锁更多用例,目标是使`Allocator`从一开始就支持动态分发。这可能是自上篇文章以来nightly版本的最大变更。
```rust
let my_allocator: Arc<dyn Allocator> = /* ... */;
let my_vec = Vec::new_in(my_allocator);
```
我在前文(https://cetra3.github.io/blog/state-of-allocators-2026#zig-s-allocator)中提及过,动态分发分配器对性能影响不大——实际上这正是Zig的默认方式。我认为考虑到灵活性,这将逐渐成为库设计中使用自定义分配器的事实标准。
### 异常展开与分配器
一项重要规范是:禁止从分配器中进行异常展开,相关表述也更加严格。此前允许展开曾导致大量内存不安全问题。例如扩展或调整`Vec`大小可能涉及多次分配/释放操作,若在这些过程中触发panic,`Vec`自身状态(指针/长度/容量)将陷入不一致,可能引发双重释放等严重问题。
## 遗漏部分
由于此变更既简单又*根本性*地影响Rust运作方式,团队投入了大量精力排查可能导致使用场景*内存不安全*的特殊情况。目前仅支持`Vec`和`Box`两种容器类型,且这些类型的部分方法未包含自定义分配器。因此可将此次稳定化视为自定义分配器的**最小可行方案**而非最终形态:它足以推动应用,但距完成仍有*大量*工作。
简言之,您可以立即开始使用,但某些用例可能受制于其他领域的稳定化进展。我将尽力说明排除部分及其原因,但issues和zulip聊天中的讨论更为深入。
### 分配器、Drop与Clone
当`Drop`一个分配器时,实际释放的是什么?是内存分配引擎还是*内存*本身?这取决于具体实现。部分分配器需要在析构时执行清理,另一些则可能不需要。
先前版本曾提供以下通用实现:
```rust
impl<T, A: Clone> Clone for Arc<T, A> { /* ... */ }
```
即若`Arc`中的自定义分配器可克隆,就能克隆整个`Arc`。但若分配器在drop时释放内存会怎样?请看以下伪代码(具体问题参见原issue(https://github.com/rust-lang/rust/issues/156920)):
```rust
// 你的分配器在drop时释放内存,克隆后共享同一存储区
let my_memory_arena = MemoryArena::new();
let the_first_arc = Arc::new_in(123456, my_memory_arena); // 我们在克隆什么?
let the_second_arc = the_first_arc.clone();
drop(the_first_arc); // 释放第一个Arc,分配器随之销毁,但the_second_arc仍存在!
drop(the_second_arc); // 释放第二个Arc,导致双重释放!
```
此处没有`unsafe`代码(除隐藏的`MemoryArena`实现外)。或许有人会说:若在`MemoryArena`或其他分配器的unsafe契约中声明克隆操作会复制内存,就能规避双重释放问题。这固然可行,但`Clone`trait本身*非*unsafe,下游用户仍可能通过`Box`包装制造不安全代码(参见原issue(https://github.com/rust-lang/rust/issues/156920)):
```rust
pub trait NewTrait: Allocator {}
impl<T: Allocator> NewTrait for T {}
impl<T> Clone for Box<dyn NewTrait> {
fn clone(&self) -> Self {
Box::new(System)
}
}
fn main() {
let the_first_arc = Arc::new_in(123456, Box::new(MemoryArena::new()) as Box<dyn Allocator>);
let the_second_arc = the_first_arc.clone();
drop(the_first_arc); // 未使用unsafe却产生内存不安全代码!
drop(the_second_arc);
}
```
那么为何需要克隆能力?除便利性外,部分容器类型(如`BTreeMap`)需要克隆分配器来支持`split_off`等操作:
```rust
let my_memory_arena = MemoryArena::new();
let mut a = BTreeMap::new_in(my_memory_arena);
a.insert(1, "a");
a.insert(2, "b");
a.insert(3, "c");
a.insert(17, "d");
a.insert(41, "e");
let b = a.split_off(&3);
drop(a); // 释放原始映射及分配器(b仍持有克隆版本)
drop(b); // 丢失底层内存!
```
因此我们需要允许克隆分配器,但必须附加额外约束。当前设计(仍可能调整)引入新的unsafe标记trait`AllocatorClone`,用于提供必要保证(如drop时不释放内存)。通过此trait,可将约束移入unsafe部分,避免在安全Rust中引入潜在不安全代码。
### Pin与内存型变
这部分较难理解,我将结合issue示例(https://github.com/rust-lang/rust/issues/157089)说明,其逻辑与克隆需要更强保证类似。
#### 型变
首先解释*型变*概念:
```rust
let static_str: &'static str = "hello world!";
let shorter: &'a str = static_str; // 可行!因协变性!
```
虽然`'static`与`'a`不同,但`'static`是`'a`的子类型。由于`&'a str`对`'a`具有协变性,可将较长生命周期替代较短生命周期使用。
#### Pin保证
多数人认为`Pin`是永不*移动*的类型,但分配与释放仍需处理。此外,被pin的数据必须在内存失效*前*完成drop:
- 必须在内存中保持不动
- 必须在内存失效前调用drop
此约束确保依赖pin状态的代码能在值销毁前清理数据结构。但`Drop::drop`并非必须调用——可通过`ManuallyDrop`或内存泄漏跳过。讽刺的是,泄漏pin值仍满足保证(因未失效内存)。
#### Box示例
以下简化示例基于issue(https://github.com/rust-lang/rust/issues/157089)中的`Bump`分配器:
```rust
let mut arena = Bump::new();
let my_value = Box::new_in(Thing::new(), &arena);
```
由于传入`&arena`,生命周期规则可阻止在分配存活期间调用`reset()`:
```rust
let my_value = Box::new_in(Thing::new(), &arena);
std::mem::forget(my_value); // 遗忘值
arena.reset(); // 值已遗忘,可安全重置
```
若要*pin*此值,需确保在内存失效前调用析构器。理想情况下,`Box::pin_in`应保证对`Bump`分配器无法调用`reset()`。一种方案是将分配器设为`'static`:
```rust
pub fn pin_in<T, A: Allocator>(x: T, alloc: A) -> Pin<Box<T, A>>
where
A: 'static + Allocator, // 需为'static
{
Self::into_pin(Self::new_in(x, alloc))
}
```
这样无法编译:
```rust
let mut arena = Bump::new();
let pinned = Box::pin_in(Thing::new(), &arena); // 编译失败,&arena非'static
```
若可编译,则可能写出不安全代码:
```rust
std::mem::forget(pinned);
arena.reset(); // 在drop前失效内存!破坏pin保证
```
可通过泄漏分配器满足约束:
```rust
let static_arena: &'static Bump = Box::leak(Box::new(Bump::new()));
let pinned = Box::pin_in(Thing::new(), &static_arena); // 可行,内存已泄漏但未失效
std::mem::forget(pinned);
```
但借助*协变性*,可将`'static`转换为其他生命周期:
```rust
let static_arena: &'static Bump = Box::leak(Box::new(Bump::new()));
let pinned: Pin<Box<Thing<'static>, &'static Bump>> = Box::pin_in(Thing::new(), &static_arena);
let pinned_coerced: Pin<Box<Thing<'a>, &'a Bump>> = pinned; // 协变转换
```
看似无害?实则与`Clone`交互会引发问题。`Box`作为`#[fundamental]`类型可实现自定义Clone:
```rust
struct Thing<'a> {
alloc: &'a Bump,
_pin: PhantomPinned,
}
impl<'a> Clone for Box<Thing<'a>, &'a Bump> {
fn clone(&self) -> Self {
Box::new_in(Thing { alloc: self.alloc, _pin: PhantomPinned }, self.alloc)
}
}
```
结合协变性即可构造不安全代码:
```rust
let static_arena: &'static Bump = Box::leak(Box::new(Bump::new()));
let mut short_arena = Bump::new();
// ... 后续可利用生命周期转换破坏pin保证 ...
```
(后续内容将展示如何通过分配器交互与生命周期操纵突破约束)
相似文章
你的Rust服务没有内存泄漏——可能是分配器的问题
本文描述了一次调试过程:一个Rust服务在负载下内存持续居高不下,但并没有真正泄漏,原因是glibc的ptmalloc分配器没有将释放的内存归还给操作系统。文章解释了分配器的行为,并为Rust开发者提供了见解。
静态分配,恒定工作
本文探讨了静态分配策略,以防止释放后使用、类型混淆等内存安全问题,讨论了对象池和代际索引,并介绍了来自TigerStyle的技巧,以在初始化后避免动态内存分配。
安全 Rust 的边界
TokioConf 2026 的一篇演讲/博客文章探讨了如何通过为复杂指针结构实现追踪式垃圾回收,将安全 Rust 推向极限,并分享处理循环引用与原始指针 GC 设计的技巧。
Pony 的区域分配器
这篇文章描述了一种为 Pony 语言设计的新区域分配器,它解决了在压力测试中发现的内存增长和性能问题,设计灵感来源于 snmalloc。
优化 LLVM 的 bump 分配器
这篇博客文章详细介绍了对 LLVM 的 BumpPtrAllocator 进行的三项近期优化,通过移除冗余对齐、空指针检查以及每次分配的记账开销来减少快速路径开销,从而提升了 Clang、lld 及其他 LLVM 组件的性能。