< 返回版块

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 双重策略设计

做减法?不,无论是读还是写,我们全都要。smr-swap 现在提供两种截然不同的策略以适应不同的场景:

  1. 写优先 (Write-Preferred,默认)

    • 均衡之道:使用对称内存屏障 (Symmetric Memory Barriers)。读写操作均承担常规的同步开销。
    • 快速写入:在混合读写和多写者场景下,其写入速度显著快于读优先策略。
    • 优秀读取:读取延迟 (~4.5ns) 依然比 ArcSwap (~9.2ns) 快约 一倍。
  2. 读优先 (Read-Preferred,Feature read-preferred)

    • 极致读取:使用非对称内存屏障 (Asymmetric Memory Barriers)。同步开销几乎全部转移到了写入端。
    • 亚纳秒级:读取延迟几乎可以忽略不计 (~0.9ns)。
    • 写入略慢:由于重量级屏障和读者状态检查,写入操作开销较大。

3.2 读操作 (读优先策略)

对于"读优先"策略,读取路径被设计为绝对的最短路径:

  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-Swap (两种策略) 与 ArcSwapMutex 的对比。

4.1 延迟特性分析

场景 SMR-Swap (写优先) SMR-Swap (读优先) ArcSwap Mutex
单线程读取 4.49 ns 0.90 ns 9.19 ns ~20 ns
多线程读取 (8T) 5.10 ns 0.94 ns 9.60 ns N/A
混合负载 (1W+2R) 66.10 ns 86.01 ns 428.14 ns ~1.5 µs
混合负载 (1W+8R) 76.63 ns 86.75 ns 502.69 ns ~1.5 µs
单线程写入 54.84 ns 89.81 ns 104.79 ns ~20 ns

分析结论

  • 写优先 (默认):瞄准了 "Sweet Spot"。它在提供优秀的读取性能(延迟仅为 ArcSwap 的一半)的同时,保持了极快且可扩展的写入性能(在混合负载下显著超越 ArcSwap)。
  • 读优先:读多写少场景的王者。如果你的应用 99% 以上的时间都在读取,该策略能提供亚纳秒级的延迟,带来数量级的性能优势。

4.2 开销权衡 (Trade-off)

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

操作 SMR-Swap ArcSwap 说明
句柄创建 ~152 ns ~130 ns 需分配并注册 ReaderSlot
句柄销毁 ~81 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

评论区

写评论
bestgopher 2025-12-26 15:15

6

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

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

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 共 4 条评论, 1 页