< 返回版块

ShaoG-R 发表于 2025-12-17 21:29

Tags:并发编程, 性能优化, 无锁数据结构, 内存管理

大家好,我是 Shao G.

在高性能并发系统的构建中,"单写多读"(Single-Writer Multi-Reader, SWMR)是一种极其常见的访问模式。针对这一场景,Rust 标准库提供了 RwLock,社区中也有广受好评的 arc-swap

然而,在对延迟极其敏感的场景下(如高频配置读取、实时渲染状态同步),即便是 Wait-Freearc-swap 也仍有优化空间。本文将介绍 smr-swap 库,它通过一种基于版本的内存回收机制(Version-Based Memory Reclamation),实现了亚纳秒级的读取延迟,并在设计哲学上选择了"性能优先"的路径。

一、 引言:关于"轮子"的思考

在介绍 smr-swap 之前,有必要探讨一下为何我们需要一个新的并发原语。

1.1 现有方案的局限性

在 Rust 中处理 SWMR 场景,我们通常面临两个选择:

  1. RwLock<T>

    • 优点:语义标准,使用简单。
    • 缺点:读操作需要 CAS 或原子加减,在激烈竞争下会导致 cache line 颠簸;且写锁会阻塞读锁。虽然 parking_lot 优化了性能,但本质开销依然存在。
  2. ArcSwap<T>

    • 优点:实现了无锁(Wait-Free)读取,性能优异。
    • 缺点:类型系统复杂度较高。

1.2 类型复杂度的熵增

在使用 ArcSwap 构建共享数据结构时,为了满足线程共享(Arc)和低成本快照(避免 Clone 大结构),开发者往往被迫构建出多层嵌套的类型:

// 典型的三层嵌套
let data: Arc<ArcSwapAny<Arc<HashMap<K, V>>>> = ...;

// 如果为了 Clone 优化引入 immutable collections
let data: Arc<ArcSwapAny<Arc<im::HashMap<K, V>>>> = ...;

这种"俄罗斯套娃"式的类型签名不仅在视觉上不够优雅,更在语义上模糊了所有权修改源的界限。外层 Arc 的存在暗示了多处持有的可能性,掩盖了"单写"的本质约束。

二、 设计哲学与偶然发现

smr-swap 的诞生源于一次对 Epoch-based Synchronization(基于周期的同步)机制的探索性研究。

2.1 性能优先原则

不同于 ArcSwap 追求通用性和易用性,smr-swap 在设计之初就确立了 "Performance First"(性能优先) 的原则。我们假设:

如果用户愿意为了纳秒级的性能提升而付出额外的编码成本(如显式的句柄管理),我们能否突破现有的性能天花板?

2.2 意外的性能收益

在实现过程中,我们采用了基于版本的 GC 策略。最初的预期仅是实现一个轻量级的无锁结构,但基准测试的结果令人惊讶:在纯读取场景下,其延迟仅为 arc-swap 的十分之一(约 0.9ns vs 9.0ns)。

这一结果并非源于复杂的算法创新,而是将运行时开销前置的设计决策所带来的红利。

三、 SMR-Swap 的核心机制

smr-swap 的核心在于其底层引擎 swmr-cell,它实现了一套精简的基于版本的内存回收机制。

3.1 核心组件

  • Global Version (全局版本):一个原子递增的计数器,代表数据的当前代际。
  • Local Reader (本地读者):每个读取线程持有的独立句柄,包含一个缓存行对齐的 ReaderSlot
  • Garbage Queue (垃圾队列):存储被替换的历史数据及其关联版本。

3.2 读操作:极致的 Wait-Free

读取路径被设计为绝对的最短路径:

  1. Pinning: 读取者将当前的 Global Version 复制到自己的 ReaderSlot 中。这是一个简单的 Atomic Store。
  2. Load: 读取者加载数据指针。这是一个 Atomic Load。
  3. Access: 通过 RAII 守卫访问数据。

整个过程不包含 CAS 循环,不包含重试逻辑,也不需要复杂的内存屏障握手。这种"快照式"的注册机制,结合独立的 Cache Line,几乎完全消除了读者之间的竞争。

3.3 写操作与 GC

写者负责数据的原子交换和垃圾回收:

  1. Publish: 原子交换数据指针,并递增全局版本号。
  2. Reclaim: 扫描所有活跃读者的 ReaderSlot,计算出最小活跃版本 (Min Active Version)。任何版本小于该值的垃圾对象都将被安全释放。

四、 性能评估

我们在 Intel Core i9-13900KS 平台上进行了严格的基准测试。以下数据展示了 SMR-SwapArcSwapMutex 的对比。

4.1 延迟特性分析

场景 SMR-Swap ArcSwap Mutex 性能差异
单线程读取 0.90 ns 9.20 ns ~20 ns 10x 提升
多线程读取 (8T) 0.93 ns 9.75 ns N/A 10x 提升
混合负载 (1W+8R) 58.21 ns 509.31 ns ~1.5 µs 89% 提升
单线程写入 66.61 ns 104.97 ns ~20 ns 快 37% (vs ArcSwap)

分析结论

  • 在读取密集型负载下,smr-swap 展现出数量级的优势,延迟稳定在 1ns 以下。
  • 在混合读写场景下,由于 GC 是分批异步进行的,且写不阻塞读,整体吞吐量依然保持在高位。

4.2 开销权衡 (Trade-off)

性能的提升并非没有代价。SMR-Swap 遵循运行时成本前置的原则,导致其初始化和销毁成本相对较高。

操作 SMR-Swap ArcSwap 说明
句柄创建 ~214 ns ~130 ns 需分配并注册 ReaderSlot
句柄销毁 ~65 ns ~110 ns 需注销 Slot
运行时检查 ~0.2 ns N/A 本地状态检查几乎无感

这决定了 SMR-Swap 的最佳适用场景是长生命周期的任务(如服务器工作线程),而非频繁创建销毁的短任务。

五、 生态组件:swmr-cell 与 lfrlock

为了构建完整的解决方案,我们提供了分层的组件生态:

5.1 swmr-cell (Core Engine)

这是底层的 unsafe 原语,提供了最细粒度的控制。它负责指针管理、版本维护和内存回收。如果你需要构建自定义的并发数据结构,可以直接基于 swmr-cell 开发。

5.2 lfrlock (Lock-Free Read Lock)

针对需要多写者的场景,我们设计了 lfrlock

  • 定义LfrLock<T> = Mutex<T> (Write) + Wait-Free (Read)
  • 特性:写入串行化,读取无锁化。
  • 约束:由于内部持有 Thread-Local 状态,LfrLock!Sync 的。

注意

  1. 简易封装LfrLock 仅仅是 smr-swap 的一个便捷包装。对于需要更复杂的线程间共享模型,或者需要精细控制 LocalReader 生命周期的场景,我们强烈建议直接使用 Arc<Mutex<SmrSwap<T>>> 并手动管理读者的创建与分发,这样能获得更大的灵活性。
  2. 非 Drop-in Replacement:它要求使用者在多线程环境下通过克隆句柄(而非共享引用)来工作。
  3. 写操作成本:由于写操作涉及 Clone -> Modify -> Swap 流程,对于 Cloning 成本巨大的类型需谨慎评估。

六、 No-std 支持

swmr-swapswmr-celllfrlock 目前均已支持 no_std 环境。

lfrlock 支持 no_std 环境。要在 no_std crate 中使用它:

  1. 禁用默认特性。
  2. 如果需要 Mutex 支持(LfrLock 用于写入),请启用 spin 特性。
  3. 确保 alloc 可用。
[dependencies]
lfrlock = { version = "0.2", default-features = false, features = ["spin"] }

注意:LfrLock 依赖 Mutex 来串行化写入。在 std 环境中,它使用 std::sync::Mutex。在启用了 spin 特性的 no_std 环境中,它使用 spin::Mutex

七、 结语

smr-swap 是我们在 Rust 高性能并发领域的一次探索。它证明了通过合理的 API 约束和设计取舍(如通过 !Sync 换取 Thread-Local 优化,通过增加初始化开销换取运行时性能),我们仍然可以大幅突破现有的性能边界。

如果你的系统对延迟有着严苛的要求,或者你正在寻找一个语义更清晰的单写多读方案,欢迎尝试 smr-swap


Ext Link: https://github.com/ShaoG-R/smr-swap

评论区

写评论
Jeff子福 2025-12-18 17:03

创造轮子不易,高低得点个赞

作者 ShaoG-R 2025-12-18 11:02

是类似于crossbeam-epoch,我最早实现是设计了一个swmr特化版本的epoch based内存回收,这里的version based只是将维护全局epoch简化成了维护对象版本,只回收历史对象版本小于安全回收版本的对象

--
👇
bestgopher: 类似 crossbeam-epoch 这样的吗

bestgopher 2025-12-18 10:54

类似 crossbeam-epoch 这样的吗

1 共 3 条评论, 1 页