2026年分配器的现状 - 六个月后

Lobsters Hottest 新闻

摘要

本文更新了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服务没有内存泄漏——可能是分配器的问题

Lobsters Hottest

本文描述了一次调试过程:一个Rust服务在负载下内存持续居高不下,但并没有真正泄漏,原因是glibc的ptmalloc分配器没有将释放的内存归还给操作系统。文章解释了分配器的行为,并为Rust开发者提供了见解。

静态分配,恒定工作

Lobsters Hottest

本文探讨了静态分配策略,以防止释放后使用、类型混淆等内存安全问题,讨论了对象池和代际索引,并介绍了来自TigerStyle的技巧,以在初始化后避免动态内存分配。

安全 Rust 的边界

Lobsters Hottest

TokioConf 2026 的一篇演讲/博客文章探讨了如何通过为复杂指针结构实现追踪式垃圾回收,将安全 Rust 推向极限,并分享处理循环引用与原始指针 GC 设计的技巧。

Pony 的区域分配器

Lobsters Hottest

这篇文章描述了一种为 Pony 语言设计的新区域分配器,它解决了在压力测试中发现的内存增长和性能问题,设计灵感来源于 snmalloc。

优化 LLVM 的 bump 分配器

Lobsters Hottest

这篇博客文章详细介绍了对 LLVM 的 BumpPtrAllocator 进行的三项近期优化,通过移除冗余对齐、空指针检查以及每次分配的记账开销来减少快速路径开销,从而提升了 Clang、lld 及其他 LLVM 组件的性能。