aw0lid/InlineCollections

GitHub: aw0lid/InlineCollections

为 .NET 提供基于栈分配的零堆分配高性能内联集合基元,解决热路径中小型临时集合的 GC 开销和不可预测延迟问题。

Stars: 1 | Forks: 0

[![NuGet 版本](https://img.shields.io/nuget/v/InlineCollections.svg?style=flat-square)](https://www.nuget.org/packages/InlineCollections/) ![.NET 8.0+](https://img.shields.io/badge/.NET-8.0%2B-blue) ![许可证: MIT](https://img.shields.io/badge/License-MIT-yellow.svg) ![分配: 零](https://img.shields.io/badge/Allocations-Zero-green) ![性能: 超低延迟](https://img.shields.io/badge/Performance-Ultra--Low--Latency-orange) # ⚡ 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压力, 栈分配, 游戏开发, 热路径, 系统级编程, 缓存优化, 集合类, 零内存分配, 高频交易