aw0lid/InlineCollections
GitHub: aw0lid/InlineCollections
为 .NET 提供基于栈分配的零堆分配高性能内联集合基元,解决热路径中小型临时集合的 GC 开销和不可预测延迟问题。
Stars: 1 | Forks: 0
[](https://www.nuget.org/packages/InlineCollections/)




# ⚡ InlineCollections
InlineCollections 为 .NET 提供了高性能、零分配的集合基元,其固定容量为 8、16 或 32 个元素。这些集合被实现为 `ref struct` 类型,专为必须消除堆分配的超低延迟场景进行了优化。
## 🚀 概述
InlineCollections 提供了**三种集合类型**(`InlineList`、`InlineStack`、`InlineQueue`),分为**三种固定大小**(8、16 和 32 个元素),通过 `InlineArray` 语言特性 (C# 12+) 实现栈分配存储,从而实现:
- ✨ 快速路径零堆分配
- 🏎️ 最小的 GC 压力
- 🎯 用于缓存优化的可预测内存布局
- 🛡️ 高吞吐量、低延迟执行
## 🛠️ 入门指南
### 📦 安装
通过 .NET CLI 将包添加到您的项目中:
```
dotnet add package InlineCollections --version 0.2.0
```
## 🚀 快速入门与用法
`InlineCollections` 是专为栈分配设计的 `ref struct` 类型。它们确保最多 32 个元素的**零堆分配**。
```
using InlineCollections;
// Initialize on stack (Zero Allocation)
var list = new InlineList32();
list.Add(10);
list.Add(20);
// High-performance iteration (Modify in-place via Span)
foreach (ref int item in list.AsSpan())
{
item += 1;
}
// ⚠️ When using large value types as elements, be aware that every
// pass-by-value of `list` copies the entire buffer (capacity * sizeof(T)).
// Passing by `ref` or `in` can avoid excessive copying on the stack.
```
## 💡 为什么存在这个库
标准的 .NET 集合(`List`、`Stack`、`Queue`)会在堆上进行分配,对于较小的工作集会导致 GC 开销和缓存未命中。在高性能场景(实时系统、游戏引擎、网络处理器、序列化热点)中,这种开销是不可接受的。InlineCollections 通过将 32 个元素直接内联存储在结构体本身中来消除分配。
## 何时使用
- ⚡ 创建许多短生命周期小型集合(≤ 32 个元素)的热路径代码
- ⏱️ 需要可预测延迟的实时系统
- 🎮 游戏引擎帧局部处理(每帧临时缓冲区)
- ⚡ 网络数据包处理和协议解析
- 🔁 分配情况至关重要的序列化/反序列化缓冲区
- 🧠 具有有限深度(8-32 个元素)的类似栈或帧局部的数据
## 何时不使用
- 集合 routinely 超过 32 个元素的情况
- 需要线程安全或并发访问的场景
- 引用类型或可为空的元素类型
- 需要与 `List` 进行 API 兼容的情况
- GC 压力不是主要关注点的托管堆场景
- 集合大小无法静态限制的情况
## 工作原理
### 内存模型
每种集合类型通过 `InlineArray8`、`InlineArray16` 或 `InlineArray32` 结构体使用内联存储,它们利用 `[InlineArray(N)]` 属性将指定数量的元素直接嵌入到结构体内部。这是一个存储在栈上的值类型集合(当未被捕获在引用类型中时)。
- 内联存储:N 个元素作为结构体字段存储(其中 N ∈ {8, 16, 32})
- 无堆分配
- `ref struct` 语义(无装箱,无引用存储)
### 约束
- 仅限非托管元素类型(约束:`T : unmanaged, IEquatable`)
- 固定容量恰好为 32 个元素
- 超出容量时抛出 `InvalidOperationException`
- 值类型语义:在赋值/参数传递时进行复制
## 提供的集合
### InlineList8, InlineList16, InlineList32
分别具有最大容量为 8、16 或 32 个非托管元素的列表。
**关键方法:**
- `Add(T item)` — 添加到末尾;如果已满则抛出异常
- `TryAdd(T item)` — 添加到末尾;如果已满则返回 false
- `AddRange(ReadOnlySpan items)` — 批量添加
- `Insert(int index, T item)` — 在指定索引处插入
- `TryInsert(int index, T item)` — 在指定索引处插入;如果无效或已满则返回 false
- `Remove(T item)` — 移除第一个匹配项
- `RemoveAt(int index)` — 按索引移除
- `T this[int index]` — 带有 ref 返回的索引器,用于就地修改
- `Span AsSpan()` — 将当前元素作为 span 获取
- `Contains(T item)` — 线性搜索
- `Clear()` — 清空列表
- `int Count` — 获取当前元素数量
- `const int Capacity` — 固定的最大容量(8、16 或 32)
**示例:**
```
var list = new InlineList16();
list.Add(1);
list.Add(2);
list.Add(3);
int value = list[0]; // 1
var span = list.AsSpan();
foreach (var item in span) {
Console.WriteLine(item);
}
foreach (var item in list) {
Console.WriteLine(item);
}
```
### InlineStack8, InlineStack16, InlineStack32
最大容量为 8、16 或 32 个元素的 LIFO(后进先出)集合。
**关键方法:**
- `Push(T item)` — 压入栈;如果已满则抛出异常
- `TryPush(T item)` — 压入栈;如果已满则返回 false
- `T Pop()` — 弹出并返回;如果为空则抛出异常
- `bool TryPop(out T result)` — 安全弹出
- `ref T Peek()` — 返回顶部的引用但不移除;如果为空则抛出异常
- `Span AsSpan()` — 获取所有元素(按插入顺序)
- `Clear()` — 清空栈
- `int Count` — 获取当前元素数量
- `const int Capacity` — 固定的最大容量(8、16 或 32)
**示例:**
```
var stack = new InlineStack16();
stack.Push(10);
stack.Push(20);
stack.Push(30);
int top = stack.Pop(); // 30
int next = stack.Pop(); // 20
ref int peek = ref stack.Peek();
peek = 100; // modify in-place
foreach (var item in stack) {
Console.WriteLine(item); // Iterates in reverse order (LIFO)
}
```
### InlineQueue8, InlineQueue16, InlineQueue32
最大容量为 8、16 或 32 个元素的 FIFO(先进先出)集合。内部使用循环缓冲区实现 O(1) 的入队/出队操作。
**关键方法:**
- `Enqueue(T item)` — 添加到尾部;如果已满则抛出异常
- `TryEnqueue(T item)` — 安全添加;如果已满则返回 false
- `T Dequeue()` — 从头部移除并返回;如果为空则抛出异常
- `bool TryDequeue(out T result)` — 安全出队
- `ref T Peek()` — 返回头部的引用但不移除;如果为空则抛出异常
- `Clear()` — 清空队列
- `int Count` — 获取当前元素数量
- `const int Capacity` — 固定的最大容量(8、16 或 32)
**示例:**
```
var queue = new InlineQueue16();
queue.Enqueue(1);
queue.Enqueue(2);
queue.Enqueue(3);
int first = queue.Dequeue(); // 1
int second = queue.Dequeue(); // 2
ref int front = ref queue.Peek();
front = 999; // modify in-place
foreach (var item in queue) {
Console.WriteLine(item); // Iterates in FIFO order
}
```
## 限制与异常
- **固定容量**:根据集合变体恰好为 8、16 或 32 个元素;超出容量将抛出 `InvalidOperationException`。
- **仅限非托管类型**:`T` 必须满足 `T : unmanaged, IEquatable`。
- **值语义**:赋值和参数传递会复制整个结构体。
- **结构体大小**:每个集合占用(容量 * sizeof(T))字节加上开销。例如:
- `InlineList8`:32 字节 + 4 字节 (count) = 36 字节
- `InlineList32`:128 字节 + 4 字节 (count) = 132 字节
- ⚠️ **栈内存警告**:由于存储是内联的,使用大型非托管元素类型会增加结构体的栈占用量,可能会导致显著的栈压力或 `StackOverflowException`。考虑使用较小的大小变体、基于堆的集合,或者通过 `ref`/`in` 传递结构体以避免昂贵的复制操作。
- **ref struct**:不能存储在引用类型或类的字段中;不能被装箱。
- **不允许 null 元素**:元素必须是有效的非托管值。
- **异常**:
- `InvalidOperationException` — 超出容量或集合为空(在没有 Try- 变体的情况下执行 Pop/Peek/Dequeue 时)
- `ArgumentOutOfRangeException` — 无效索引 (RemoveAt)
## 性能特征
除以下操作外,所有操作均为 O(1) 常量时间:
- `Remove(T item)` — O(n) 线性搜索和移位
- `RemoveAt(int index)` — O(n) 移位剩余元素
- `Insert(int index, T item)` — O(n) 将元素向右移位
索引器访问(`this[int index]`)和 Peek/Pop 操作没有边界检查,并且被积极内联。
有关与 `List`、`Stack` 和 `Queue` 的详细基准测试和比较,请参阅 [docs/benchmarks.md](docs/benchmarks.md)。
## 文档
- [架构](docs/architecture.md) — 内部设计与内存布局
- [设计理念](docs/design-philosophy.md) — 原则与目标
- [内存模型](docs/memory-model.md) — 栈与堆,值语义,ref 安全性
- [集合](docs/collections/) — 各集合 API 参考与示例
- [InlineList32](docs/collections/inline-list.md)
- [InlineStack32](docs/collections/inline-stack.md)
- [InlineQueue32](docs/collections/inline-queue.md)
- [性能](docs/performance.md) — 基准测试方法与结果
- [限制](docs/limitations.md) — 硬性约束与异常
- [何时使用](docs/when-to-use.md) — 推荐场景
- [何时不使用](docs/when-not-to-use.md) — 应避免的场景
- [常见问题](docs/faq.md) — 常见疑问
## 基准测试
将 InlineCollections 与标准 BCL 集合进行比较的 BenchmarkDotNet 结果可在 `benchmarks/` 目录中找到。运行:
```
dotnet run --project benchmarks/InlineCollections.Benchmarks -c Release
```
主要发现:
- **添加操作**:对于小型集合(零分配),InlineList32 比 List 快 3-5 倍
- **索引器访问**:与 List 性能几乎一致(两者都使用直接内存访问)
- **内存**:零堆分配,而 List 需要一次分配
完整结果请参见 [docs/benchmarks.md](docs/benchmarks.md)。
## InlineCollections 是什么
- 为高性能系统中的热路径进行了优化
- 面向小型集合的零分配基元
- 具有栈分配语义的引用类型级性能
- 确定性的且对缓存友好
## InlineCollections 不是什么
- `List`、`Stack` 或 `Queue` 的替代品
- 线程安全或并发集合
- 用于任意工作负载的通用数据结构
- 垃圾回收或引用类型的集合
## 权衡
### 内联与堆
**内联**:
- ✅ 无分配
- ✅ 缓存友好
- ✅ 快速的小型操作
- ❌ 固定容量
- ❌ 结构体复制成本
- ❌ 栈大小成本
**堆**:
- ✅ 动态容量
- ✅ 无限制增长
- ✅ 无复制(引用类型)
- ❌ 分配开销
- ❌ GC 压力
- ❌ 缓存未命中
### 非安全优化
**边界检查**:
- ✅ 默认安全
- ❌ 分支预测失败的开销
**不检查**:
- ✅ 热循环中 0 个分支
- ✅ 更快的索引访问
- ❌ 调用方必须确保索引有效(在实践中,这通常是有保证的)
标签:C# 12, InlineArray, NuGet, ref struct, Span<T>, 低延迟, 内存优化, 内联集合, 吞吐量, 数据结构, 无GC压力, 栈分配, 游戏开发, 热路径, 系统级编程, 缓存优化, 集合类, 零内存分配, 高频交易