深入 polars-buffer:Polars 引擎中 O(1) 共享不可变缓冲区的实现原理
本篇聚焦 Polars 的内部子 crate polars-buffer,解读它如何为整个列式查询引擎提供"可跨线程共享、切片与克隆均为 O(1) 的不可变缓冲区"。读完后,你将理解 Buffer<T> 与 SharedStorage<T> 的结构设计、引用计数与独占可写判定的实现细节,以及零值缓冲池等关键优化,并能结合源码与测试确认其行为边界。
1. crate 定位:为什么 Polars 需要一层自研缓冲区
根据 crates/polars-buffer/README.md,polars-buffer 是 Polars 库的一个内部子 crate,负责提供"底层数据可共享的不可变缓冲区(immutable buffers)"的实现,并且明确声明不面向外部用户——面向用户的功能应使用主 polars crate。它的 Cargo 描述(crates/polars-buffer/Cargo.toml)也印证了这一点:Data buffers for the Polars DataFrame library。
这个 crate 解决的正是列式引擎的核心问题:一份数据需要在多个地方以零拷贝方式被引用。在 Polars 中,所有不可变 Arrow 数组都由 Buffer 支撑——crates/polars-arrow/src/array/mod.rs 的模块文档直接写明:"All immutable arrays are backed by Buffer and thus cloning and slicing them is O(1)"。也就是说,上层 DataFrame 做切片、投影、按块处理时,绝大多数操作只是移动指针与长度,而不是复制数据。
依赖面非常精简:仅 bytemuck(POD 类型转置)、either(into_mut 的双结果返回)、polars-utils(range 校验工具),以及可选的 serde/schemars 特征(crates/polars-buffer/Cargo.toml)。crate 入口(crates/polars-buffer/src/lib.rs)只导出两个类型:
pub mod buffer;
pub mod storage;
pub use buffer::Buffer;
pub use storage::SharedStorage;
2. Buffer:等价于 Arc<Vec>,但切片是 O(1)
核心类型定义在 crates/polars-buffer/src/buffer.rs:
pub struct Buffer<T> {
/// The internal byte buffer.
storage: SharedStorage<T>,
/// A pointer into the buffer where our data starts.
ptr: *const T,
// The length of the buffer.
length: usize,
}
源码注释给出最直观的类比:可以把 Buffer<T> 理解为 Arc<Vec<T>> 的等价物,但有两个关键差异(crates/polars-buffer/src/buffer.rs):
- 切片与克隆都是 O(1):
Buffer本身只是(storage 句柄, 起始指针, 长度)三元组。Clone的实现就是复制这三个字段(crates/polars-buffer/src/buffer.rs),不复制任何数据; - 支持外部分配的内存:不仅底层可以是
Vec<T>,还可以是外部 owner、静态数据或 FFI 导入的内存。
unsafe impl<T: Send + Sync> Send/Sync for Buffer<T>(crates/polars-buffer/src/buffer.rs)则保证了它可以安全地跨线程共享,这是引擎并行调度 morsel 的底层前提。
2.1 创建方式
Buffer 提供了与底层存储来源一一对应的构造方法(crates/polars-buffer/src/buffer.rs):
| 方法 | 底层存储 | 说明 |
|---|---|---|
Buffer::new() |
空存储(静态空内层) | 零长度 buffer,const 可调用 |
Buffer::from_static(&'static [T]) |
外部静态数据 | 借用静态生命周期切片 |
Buffer::from_vec(Vec<T>) / From<Vec<T>> |
拥有的 Vec |
最常用的方式,如 vec![1, 2, 3].into() |
Buffer::from_owner(O) |
外部 owner(AsRef<[T]>) |
owner 存入 Box<dyn Any>,随 buffer 一起析构 |
Buffer::with_slice / with_vec |
临时借用 | 闭包执行期间临时借用切片/Vec,闭包结束时若仍有存活克隆则 abort/panic |
Buffer::with_slice(crates/polars-buffer/src/buffer.rs)是一个典型的 RAII 模式:把裸切片包装成带引用计数的 storage 交给闭包,用 AbortIfNotExclusive 守卫(见下文 SharedStorage::with_slice)确保闭包返回时没有任何克隆泄漏,从而避免裸指针逃逸导致的悬垂引用。
2.2 切片、拆分与"独占才可写"
切片是最核心的操作。sliced(range) 通过 slice_in_place 把起始指针 ptr 前移、缩短 length,完全不动底层数据(crates/polars-buffer/src/buffer.rs)。性能敏感路径还提供了 sliced_unchecked / slice_in_place_unchecked,由调用方保证范围合法。相关的还有:
is_sliced():判断self.storage.len() != self.length,即当前 buffer 是否是更大的底层存储的一部分;expand_end_to_storage():保持偏移不变,把长度扩展到底层存储的末尾;split_at(mid)/split_off(at):把一个 buffer 拆成两个共享同一存储的 buffer(crates/polars-buffer/src/buffer.rs)。
可变访问被引用计数严格守护,这是"不可变缓冲区"语义的关键:
into_mut()返回Either<Self, Vec<T>>:仅当底层是Vec、无存活克隆、且未被切片时,才能把Vec"偷"回来(Either::Right),否则原样返回(Either::Left);get_mut_slice()返回Option<&mut [T]>:条件同样是无存活克隆且来自Vec(crates/polars-buffer/src/buffer.rs)。
文档示例(crates/polars-buffer/src/buffer.rs)演示了这条规则:
use polars_buffer::Buffer;
let mut buffer: Buffer<u32> = vec![1, 2, 3].into();
assert_eq!(buffer.as_ref(), [1, 2, 3].as_ref());
// it supports copy-on-write semantics (i.e. back to a `Vec`)
let vec: Vec<u32> = buffer.into_mut().right().unwrap();
assert_eq!(vec, vec![1, 2, 3]);
// cloning and slicing is `O(1)` (data is shared)
let mut buffer: Buffer<u32> = vec![1, 2, 3].into();
let mut sliced = buffer.clone();
sliced.slice(1, 1);
assert_eq!(sliced.as_ref(), [2].as_ref());
// but cloning forbids getting mut since `slice` and `buffer` now share data
assert_eq!(buffer.get_mut_slice(), None);
这段示例同时说明:一旦 clone 出去共享了数据,原 buffer 就不再能拿到独占的可写切片——上层需要修改时只能显式复制(即"写时复制"语义)。
2.3 其他实用能力
storage_refcount()/is_same_buffer():查询引用计数、判断两个 buffer 是否指向完全相同的数据,供上层做共享判断(crates/polars-buffer/src/buffer.rs);try_transmute<U: Pod>():在Pod约束下把Buffer<T>转置成Buffer<U>(如按字节重新解释),长度按字节数换算;zeroed(length):为Zeroable类型创建全零 buffer(详见第 4 节的全局零值池);- 标准 trait 实现齐全:
Deref<Target = [T]>、AsRef<[T]>、FromIterator、IntoIterator(对Copy类型,IntoIter同时实现了DoubleEndedIterator与ExactSizeIterator,见 crates/polars-buffer/src/buffer.rs);在serde特征下按切片序列化(crates/polars-buffer/src/buffer.rs)。
测试用例位于 crates/polars/tests/it/arrow/buffer/immutable.rs,覆盖了 new/from_vec/from_iter/slice/debug 等基本行为,可作为验证上述 API 的入口。
3. SharedStorage:引用计数与四种底层存储
Buffer 的共享语义全部落在 crates/polars-buffer/src/storage.rs 的 SharedStorage<T> 上。它是一个 #[repr(transparent)] 的 NonNull<SharedStorageInner<T>> 包装,内层结构如下(crates/polars-buffer/src/storage.rs):
struct SharedStorageInner<T> {
ref_count: AtomicU64,
ptr: *mut T,
length_in_bytes: usize,
backing: BackingStorage,
phantom: PhantomData<T>,
}
3.1 BackingStorage:数据归谁、谁负责释放
BackingStorage 枚举(crates/polars-buffer/src/storage.rs)定义了四种底层情况,直接对应了 Buffer 各种构造方式:
| 变体 | 含义 | 析构行为 |
|---|---|---|
Vec { original_capacity, vtable } |
数据来自 Vec<T> |
按 vtable 释放元素与分配器内存 |
ForeignOwner(Box<dyn Any + Send>) |
数据由外部 owner 持有 | owner 被 drop 时顺带释放数据 |
External |
内存由外部管理(如 FFI 导入),只需 refcount 内层本身 | 不释放数据 |
Leaked |
数据与内层都被故意泄漏,不做引用计数 | 直接返回,零开销 |
Drop 实现(crates/polars-buffer/src/storage.rs)先用 mem::replace 取出 backing 再分类释放:Vec 分支先按 length_in_bytes / size_of::<T>() 对元素执行 drop_in_place,再通过 vtable 中的 drop_buffer 归还分配器内存。这里的 VecVTable(crates/polars-buffer/src/storage.rs)保存了 size、align 和一个泛型 drop 函数,使得内层结构可以用 Any/*mut () 之类的擦除指针操作,又能以正确的类型完成释放——这正是"支持外部分配内存"与 try_transmute 能力的基础:transmute 后仍保有原始类型的 size/align 与 drop 方法。
3.2 克隆与释放:Arc 语义,但带快路径
克隆(crates/polars-buffer/src/storage.rs)与 Arc<T> 相同:对 ref_count 执行 fetch_add(1, Relaxed);Leaked 变体则完全跳过计数。释放(crates/polars-buffer/src/storage.rs)同样遵循 Arc 的内存序:fetch_sub(1, Release),减到 0 后补一道 Acquire fence 再走 drop_slow。
独占判定 is_exclusive() 即 ref_count == 1(Acquire 读取,语义直接注明"copied from Arc",crates/polars-buffer/src/storage.rs)。它是第 2.2 节所有可变操作的前提。值得注意的是 Buffer::storage_refcount() 的注释也提醒:通过共享引用读取计数只能用于统计,不能用于安全性判断——别人可能在你检查之后立刻增加计数(crates/polars-buffer/src/buffer.rs)。
3.3 把 Vec "偷"回来:try_take_vec
try_take_vec(crates/polars-buffer/src/storage.rs)是 COW 语义的落点,条件有三:
is_exclusive()——没有其他引用;- backing 必须是
Vec(External/ForeignOwner/Leaked都不行); - vtable 记录的 size/align 与当前
T一致(防止 transmute 后类型不匹配)。
满足后用 Vec::from_raw_parts(ptr, len, original_capacity) 重建 Vec,并把内层清零(original_capacity = 0、length_in_bytes = 0),避免重复释放。基于此还封装了 try_as_mut_vec(),通过 SharedStorageAsVecMut(内含 ManuallyDrop<Vec<T>>,drop 时把 Vec 写回内层,crates/polars-buffer/src/storage.rs)提供"临时把存储当可变 Vec 用、结束自动还原"的 RAII 接口。
4. 零值缓冲池:8 MiB 全局零内存的巧妙复用
Buffer<T: Zeroable>::zeroed(length)(crates/polars-buffer/src/buffer.rs)实现了一个很有工程意味的优化:crate 内用 LazyLock 静态分配了一块 8 MiB 的 4096 字节对齐的全零内存(GLOBAL_ZEROES),并调用 leak() 使其永不析构、免除引用计数开销。当请求的零值缓冲满足 align_of::<T>() <= 4096 且总字节数不超过 8 MiB 时,直接指向这块共享零内存,零分配、零计数;否则退化为 bytemuck::zeroed_vec(length) 的常规路径。
结合 SharedStorage::leak()(crates/polars-buffer/src/storage.rs,要求调用时独占,将 backing 替换为 Leaked 并 mem::forget)看,这条路径省掉了每次 clone/drop 的原子操作——对于大量全零位图(如初始 validity bitmap)这类高频小分配场景非常划算。从源码结构看,这正是"零值数据内容完全相同"的缓存友好利用。
5. 在 Polars 中的实际位置与使用建议
- 调用链:
polars-arrow中各类不可变数组(primitive、utf8/binary、list、fixed_size_list 等)直接以Buffer<T>承载数据,crates/polars-arrow下有数十个模块use polars_buffer;Buffer还实现了Splitabletrait(crates/polars-arrow/src/array/mod.rs)供数组切片时同步拆分缓冲。也就是说,DataFrame 的一次零拷贝切片,最终就是若干Buffer的sliced调用。 - 测试入口:
Buffer的单元测试放在 crates/polars/tests/it/arrow/buffer/immutable.rs,随主 crate 的集成测试一起运行。 - 使用建议:与 README 的声明一致,外部用户应通过
polarscrate 使用 DataFrame/Series API;只有在为 Polars 开发自定义 Arrow 数组、扩展插件或做深度性能分析时,才需要直接理解并引用polars-buffer。阅读它时把握两条主线即可:"共享是常态,可变是例外"(引用计数 > 1 时一切 mut 路径都失败),以及 "存储来源决定释放策略"(四种BackingStorage各对应一套析构逻辑)。
综合来看,polars-buffer 虽是一个不到 1100 行的内部 crate,却浓缩了列式引擎零拷贝架构的三个核心问题:跨线程共享如何安全(原子引用计数 + Send/Sync)、切片如何零成本(指针三元组结构)、以及共享数据如何可控地回归独占(exclusive 判定 + vtable 化的 Vec 重建)。理解它,就理解了 Polars 中"复制数据"为何在大多数查询阶段根本不会发生。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00