首页
/ 深入 polars-buffer:Polars 引擎中 O(1) 共享不可变缓冲区的实现原理

深入 polars-buffer:Polars 引擎中 O(1) 共享不可变缓冲区的实现原理

2026-09-05 14:13:36作者:滕妙奇

本篇聚焦 Polars 的内部子 crate polars-buffer,解读它如何为整个列式查询引擎提供"可跨线程共享、切片与克隆均为 O(1) 的不可变缓冲区"。读完后,你将理解 Buffer<T>SharedStorage<T> 的结构设计、引用计数与独占可写判定的实现细节,以及零值缓冲池等关键优化,并能结合源码与测试确认其行为边界。

1. crate 定位:为什么 Polars 需要一层自研缓冲区

根据 crates/polars-buffer/README.mdpolars-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 类型转置)、eitherinto_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_slicecrates/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]>:条件同样是无存活克隆且来自 Veccrates/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]>FromIteratorIntoIterator(对 Copy 类型,IntoIter 同时实现了 DoubleEndedIteratorExactSizeIterator,见 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.rsSharedStorage<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 归还分配器内存。这里的 VecVTablecrates/polars-buffer/src/storage.rs)保存了 sizealign 和一个泛型 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 == 1Acquire 读取,语义直接注明"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_veccrates/polars-buffer/src/storage.rs)是 COW 语义的落点,条件有三:

  1. is_exclusive()——没有其他引用;
  2. backing 必须是 VecExternal/ForeignOwner/Leaked 都不行);
  3. vtable 记录的 size/align 与当前 T 一致(防止 transmute 后类型不匹配)。

满足后用 Vec::from_raw_parts(ptr, len, original_capacity) 重建 Vec,并把内层清零(original_capacity = 0length_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 替换为 Leakedmem::forget)看,这条路径省掉了每次 clone/drop 的原子操作——对于大量全零位图(如初始 validity bitmap)这类高频小分配场景非常划算。从源码结构看,这正是"零值数据内容完全相同"的缓存友好利用。

5. 在 Polars 中的实际位置与使用建议

  • 调用链polars-arrow 中各类不可变数组(primitive、utf8/binary、list、fixed_size_list 等)直接以 Buffer<T> 承载数据,crates/polars-arrow 下有数十个模块 use polars_bufferBuffer 还实现了 Splitable trait(crates/polars-arrow/src/array/mod.rs)供数组切片时同步拆分缓冲。也就是说,DataFrame 的一次零拷贝切片,最终就是若干 Buffersliced 调用。
  • 测试入口Buffer 的单元测试放在 crates/polars/tests/it/arrow/buffer/immutable.rs,随主 crate 的集成测试一起运行。
  • 使用建议:与 README 的声明一致,外部用户应通过 polars crate 使用 DataFrame/Series API;只有在为 Polars 开发自定义 Arrow 数组、扩展插件或做深度性能分析时,才需要直接理解并引用 polars-buffer。阅读它时把握两条主线即可:"共享是常态,可变是例外"(引用计数 > 1 时一切 mut 路径都失败),以及 "存储来源决定释放策略"(四种 BackingStorage 各对应一套析构逻辑)。

综合来看,polars-buffer 虽是一个不到 1100 行的内部 crate,却浓缩了列式引擎零拷贝架构的三个核心问题:跨线程共享如何安全(原子引用计数 + Send/Sync)、切片如何零成本(指针三元组结构)、以及共享数据如何可控地回归独占(exclusive 判定 + vtable 化的 Vec 重建)。理解它,就理解了 Polars 中"复制数据"为何在大多数查询阶段根本不会发生。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384