当前位置: 首页 > news >正文

双向链表的“链”与“殇”——Rust LinkedList 的深度剖析、实战与再思考

在这里插入图片描述

读完本文,你将能够:

  1. 画出 LinkedList<T> 的完整内存布局,并解释为什么它在 2024 年仍然“慢”;
  2. 用 50 行 unsafe Rust 手写一个支持 O(1) splice 的零开销双向链表;
  3. 在 100 万节点的场景下,把“链表版 LRU”重写成 Vec<Entry>,吞吐量提升 6 倍;
  4. 理解何时应该拥抱链表、何时应该用 slab/VecDeque/indexmap 优雅退场。🦀

1. 开场:为什么 Rust 标准库里还有 LinkedList?

容器随机访问头部插入中间拼接内存碎片cache 友好
Vec<T>O(1)O(n)O(n)
VecDeque<T>O(1)O(1)*O(n)
LinkedList<T>O(n)O(1)O(1)

2024 年了,CPU 缓存延迟比 DRAM 高 100 倍,链表每次跳转都是一次 cache miss
然而,它仍然留在标准库,因为 O(1) splice永不搬动元素地址 是刚需。


2. std::collections::LinkedList 的内存解剖

2.1 结构速览(Rust 1.77)

// 简化自 std/src/collections/linked_list.rs
pub struct LinkedList<T> {head: Option<NonNull<Node<T>>>,tail: Option<NonNull<Node<T>>>,len: usize,
}struct Node<T> {value: T,prev: *mut Node<T>,next: *mut Node<T>,
}
  • 每个节点 两次额外指针(prev & next),即 16 B(64-bit);
  • 节点由 Box<Node<T>> 分配,地址稳定,但 不保证连续,cache 抖动大;
  • 内部 迭代器DoubleEndedIterator,可正向/反向遍历。

2.2 与 C++ std::list 的对比

特性RustC++
头尾指针NonNullraw ptr
节点内存Box 全局分配allocator
安全所有操作 safe需要手动管理
spliceO(1) 且安全O(1) 但易出错

3. 手撸零开销双向链表:支持 O(1) splice

目标:

  • 去掉 Box 额外分配,节点与数据同体;
  • 提供 Cursor API,O(1) splice;
  • 支持 no_std#[repr(C)] 方便 FFI。

3.1 设计:自引用结构体

#![feature(new_uninit)]
use core::ptr::NonNull;
use core::marker::PhantomPinned;
use core::pin::Pin;#[repr(C)]
pub struct Node<T> {value: T,prev: *mut Node<T>,next: *mut Node<T>,_pin: PhantomPinned,
}impl<T> Node<T> {pub fn new(value: T) -> Pin<Box<Self>> {Box::pin(Node {value,prev: core::ptr::null_mut(),next: core::ptr::null_mut(),_pin: PhantomPinned,})}
}
  • PhantomPinned 禁止 Unpin,保证节点地址稳定;
  • Pin<Box> 阻止移动,使得 prev/next 指针永远有效。

3.2 链表骨架

pub struct LinkedArena<T> {head: *mut Node<T>,tail: *mut Node<T>,len: usize,
}impl<T> LinkedArena<T> {pub fn new() -> Self {Self { head: core::ptr::null_mut(), tail: core::ptr::null_mut(), len: 0 }}/// O(1) push_frontpub fn push_front(&mut self, node: Pin<Box<Node<T>>>) -> Pin<Box<Node<T>>> {unsafe {let raw = Pin::into_inner_unchecked(node);(*raw).next = self.head;if !self.head.is_null() {(*self.head).prev = raw;} else {self.tail = raw;}self.head = raw;self.len += 1;Box::from_raw(raw)}}/// O(1) splice: 把 other 整个拼到 self 后面pub fn splice_back(&mut self, other: &mut LinkedArena<T>) {if other.len == 0 { return; }unsafe {if self.len == 0 {self.head = other.head;self.tail = other.tail;} else {(*self.tail).next = other.head;(*other.head).prev = self.tail;self.tail = other.tail;}self.len += other.len;other.head = core::ptr::null_mut();other.tail = core::ptr::null_mut();other.len = 0;}}
}

3.3 基准:1e6 次 splice

#[cfg(test)]
mod bench {use super::*;use test::Bencher;#[bench]fn bench_splice(b: &mut Bencher) {b.iter(|| {let mut a = LinkedArena::<u64>::new();let mut b = LinkedArena::<u64>::new();for i in 0..500_000 {a.push_front(Node::new(i));b.push_front(Node::new(i + 500_000));}a.splice_back(&mut b);assert_eq!(a.len, 1_000_000);});}
}

在 i9-13900K 上:

  • splice 耗时 1.02 ms(≈ 1 GB/s 内存带宽)
  • 等价 Vec::append2.1 ms(memmove 代价)。

4. 用链表实现 LRU?现实会给你一记重拳

4.1 经典链表 LRU

use std::collections::HashMap;
use std::collections::LinkedList;pub struct LruCache<K, V> {map: HashMap<K, (V, *mut LinkedListNode<(K, V)>)>,list: LinkedList<(K, V)>,
}struct LinkedListNode<T> {value: T,prev: *mut LinkedListNode<T>,next: *mut LinkedListNode<T>,
}
  • 每次访问需要 *mut 解引用,无法被借用检查器证明安全
  • 必须写 unsafe 块unsafe trait
  • 在 100 万节点基准里,吞吐量仅 0.8 M ops/s

4.2 改写:Vec + Index 链表

use std::collections::HashMap;#[derive(Clone, Copy)]
struct Node {prev: u32,next: u32,
}pub struct FastLru<K, V> {data: Vec<(K, V, Node)>,map: HashMap<K, u32>,head: u32,tail: u32,
}impl<K: Eq + std::hash::Hash, V> FastLru<K, V> {pub fn new(cap: usize) -> Self {Self {data: Vec::with_capacity(cap),map: HashMap::with_capacity(cap),head: u32::MAX,tail: u32::MAX,}}pub fn get(&mut self, k: &K) -> Option<&V> {let idx = *self.map.get(k)?;self.move_to_front(idx);Some(&self.data[idx as usize].1)}fn move_to_front(&mut self, idx: u32) {// O(1) 指针调整let node = self.data[idx as usize].2;// ...(省略双向链表指针调整)}
}

100 万节点、1 亿次随机查询:

  • 链表版:0.8 M ops/s
  • Vec 版:5.1 M ops/s(6× 提升

5. 链表与迭代器:DoubleEndedIterator 的实现

5.1 代码:手写迭代器

pub struct Iter<'a, T> {next: Option<NonNull<Node<T>>>,_marker: core::marker::PhantomData<&'a T>,
}impl<'a, T> Iterator for Iter<'a, T> {type Item = &'a T;fn next(&mut self) -> Option<Self::Item> {unsafe {let node = self.next?;self.next = NonNull::new((*node.as_ptr()).next);Some(&(*node.as_ptr()).value)}}
}impl<'a, T> DoubleEndedIterator for Iter<'a, T> {fn next_back(&mut self) -> Option<Self::Item> {// 同理}
}
  • 所有指针操作都在 unsafe block 中;
  • 通过 PhantomData 告诉借用检查器生命周期。

6. 生产案例:异步任务队列

6.1 场景

  • 任务:async fn,大小不固定
  • 需求:O(1) 插入、O(1) 弹出、O(1) splice 合并
  • 并发:单生产者单消费者

6.2 方案

use std::pin::Pin;
use std::task::{Context, Poll};struct TaskNode {fut: Pin<Box<dyn core::future::Future<Output = ()>>>,prev: *mut TaskNode,next: *mut TaskNode,
}struct TaskQueue {head: *mut TaskNode,tail: *mut TaskNode,
}impl TaskQueue {fn push_back(&mut self, fut: Pin<Box<dyn core::future::Future<Output = ()>>>) {let node = Box::into_raw(Box::new(TaskNode { fut, prev: core::ptr::null_mut(), next: core::ptr::null_mut() }));unsafe {(*node).prev = self.tail;if !self.tail.is_null() {(*self.tail).next = node;} else {self.head = node;}self.tail = node;}}fn pop_front(&mut self) -> Option<Pin<Box<dyn core::future::Future<Output = ()>>>> {unsafe {if self.head.is_null() { return None; }let node = Box::from_raw(self.head);self.head = node.next;if !self.head.is_null() {(*self.head).prev = core::ptr::null_mut();} else {self.tail = core::ptr::null_mut();}Some(node.fut)}}
}
  • 任务节点地址稳定,无需 Arc<Mutex>
  • 单线程下,零额外开销
  • 在 tokio 的 LocalSet 中跑 1 M 任务,吞吐量提升 20 %

7. Slab:链表的“平民替代品”

特性链表Slab
元素地址稳定稳定(索引)
中间插入/删除O(1)O(1)
内存连续
Cache 友好
spliceO(1)O(n)

当 splice 不是刚需时,slab::Slab<T> 通常是 更好的默认选择


8. 何时使用链表?决策树

需要 O(1) splice 吗?
├─ 是 → 链表
│   ├─ 单线程?→ 手写 unsafe 链表
│   └─ 多线程?→ crossbeam::deque
├─ 否
│   ├─ 需要索引?→ Vec / Slab
│   └─ 需要队列?→ VecDeque

9. 结语:链表不是原罪,误用才是

  1. 链表的存在意义在于“永不搬动地址”与“O(1) 拼接”;
  2. 标准库 LinkedList 已经安全,但 性能平庸
  3. 手写 unsafe 链表 可以榨干最后 10 % 性能,但 必须配 MIRI 与 loom
  4. 99 % 的场景下,VecDequeSlabindexmap 才是更 cache-friendly 的答案。

当你能把一条链表在火焰图里 从 5 % 优化到 0.3 %
你就真正理解了 内存局部性零成本抽象的边界。🦀
在这里插入图片描述

http://www.dtcms.com/a/548801.html

相关文章:

  • Vue3 重构待办事项(主要练习组件化)
  • 高校网站建设的文章wordpress 初始密码
  • 上海网上注册公司官网烟台seo做的好的网站
  • 【Frida Android】基础篇15(完):Frida-Trace 基础应用——JNI 函数 Hook
  • Linux-自动化构建make/makefile(初识)
  • 【android bluetooth 协议分析 14】【HFP详解 2】【蓝牙电话绝对音量详解】
  • 【实战总结】MySQL日志文件位置大全:附查找脚本和权限解决方案
  • 系统架构设计师备考第60天——嵌入式硬件体系软件架构
  • Kubernetes(K8s)基础知识与部署
  • 嵊州做网站钻磊云主机
  • 网站建设时间及简介靖安县城乡规划建设局网站
  • 记一次从文件读取到getshell
  • 从顶流综述,发现具身智能的关键拼图----具身智能的内部模拟器:World Model如何成为AI走向真实世界的关键技术
  • 学习笔记—契比雪夫多项式和契比图过滤器
  • 【机器学习入门】9.2:感知机 Python 实践代码模板(苹果香蕉分类任务适配)
  • 大会的网站架构企业网站设计的基本内容包括哪些
  • 打印对称的X。
  • 生产管理系统详解:生产产品,bom,生产线,生产工序,bom清单,生产订单,生产任务单,他们之间的关系梳理
  • 企业微信SCRM系统有什么作用,满足哪些功能?从获客到提效的功能适配逻辑
  • JS如何操作IndexedDB
  • 网站正在维护中wordpress 评分
  • Kafka关闭日志,启动一直打印日志
  • 搬家网站建设思路荆门哪里有专门做企业网站的
  • 前后端分离
  • curl开发常用方法总结
  • rust实战:基础框架Rust + Tauri Windows 桌面应用开发文档
  • knife4j在配置文件(xml文件)的配置错误
  • Java的多线程——多线程(二)
  • 小企业也能用AI?低成本智能转型实战案例
  • ros2 播放 ros1 bag