博客 / 詳情

返回

告別頻繁 GC:C#.NET PooledList 的設計與使用場景

簡介

PooledList<T> 是 高性能集合類型,由 Collections.Pooled 提供,用於替代 List<T>,通過 對象池 (ArrayPool<T>) 複用內部數組來減少 GC(垃圾回收)壓力。

⚡ 核心目標:
在需要頻繁創建/銷燬 List<T> 的場景下,PooledList<T> 通過數組租借與歸還的機制避免頻繁分配內存,從而提升性能並降低 GC 負擔。

安裝

dotnet add package Collections.Pooled --version 1.0.82

添加命名空間

using Collections.Pooled;

特點

  • 數組池化:內部數組從 ArrayPool<T>.Shared(默認)或自定義池中租借,減少分配。
  • Span<T> 支持:提供 Span 屬性,直接訪問內部數組的填充部分,支持零拷貝操作。
  • IDisposable 實現:調用 Dispose() 時,返回內部數組到池中(不調用 Dispose 不會出錯,但會降低池化效果)。
  • 擴展方法:如 TryFindTryFindLast(替換標準 FindFindLast,返回 bool 以避免異常)。
  • 添加/插入 SpanAddSpanInsertSpan 方法返回一個 Span<T>,允許直接寫入內部存儲,而無需多次調用 Add
  • 構造函數選項:

    • 支持指定自定義 ArrayPool<T>
    • clearMode 參數控制數組返回池時是否清除內容(默認自動)。
    • sizeToCapacity 參數使初始 Count == Capacity,適合值類型避免不必要的零初始化。
  • ToPooledList() 擴展:從 IEnumerable<T> 快速創建 PooledList<T>
  • 性能提升:在高頻操作中,減少 GC 觸發,尤其適合循環中創建臨時列表的場景。

內部原理

普通 List<T> 內部

  • List<T> 內部維護一個 T[] 數組。
  • 當容量不足時會 申請更大數組 並 拷貝數據。
  • 對象銷燬後,這些數組最終交給 GC 回收。

PooledList<T> 內部

  • 內部數組不是直接 new 出來的,而是從 ArrayPool<T>.Shared 租借。
  • 使用結束時通過 Dispose() 方法 歸還數組,供下次複用。
  • 避免頻繁分配和回收大數組,降低 GC Gen2 壓力。

基本用法

創建與釋放

using Microsoft.Toolkit.HighPerformance.Buffers;

using var list = new PooledList<int>(); // 自動使用 ArrayPool<int>
list.Add(1);
list.Add(2);
list.Add(3);

foreach (var item in list)
{
    Console.WriteLine(item);
}
// Dispose() 會自動歸還數組
💡 推薦使用 using 確保 Dispose() 被調用,否則不會歸還數組,造成內存浪費。

初始容量

using var list = new PooledList<int>(initialCapacity: 1024);

轉換為 Span<T> / Memory<T>

PooledList<T> 的優勢之一是可以直接獲取底層內存:

Span<int> span = list.AsSpan();
Memory<int> memory = list.AsMemory();

這樣可以高效地與 Span/Memory API 交互,避免額外拷貝。

常用操作

List<T> 基本一致:

list.Add(10);
list.AddRange(new[] { 20, 30, 40 });
list.Insert(1, 15);
list.RemoveAt(0);
list.Clear();

Console.WriteLine(list.Count);
Console.WriteLine(list.Capacity);

List<T> 對比

特性 List<T> PooledList<T>
內存分配 每次擴容 new 新數組 ArrayPool<T> 租借,複用數組
GC 壓力 大量頻繁創建/銷燬時 GC 壓力大 減少 GC Gen2 壓力
釋放方式 依賴 GC 必須 Dispose() 歸還數組
性能(頻繁操作場景) 可能產生大量堆分配 高性能、低分配
Span/Memory 支持 需要 AsSpan() 擴展 內置 AsSpanAsMemory,零拷貝訪問
適用場景 通用集合 高性能、臨時性數據集合(網絡、序列化、算法)

高級用法

ArrayPool<T> 配合

using var list = new PooledList<byte>(ArrayPool<byte>.Shared, 2048);

可以傳入自定義池,比如為特殊場景優化的 ArrayPool<T>

Span<T> 高效處理

適合序列化/反序列化:

Span<byte> buffer = list.AsSpan();
ProcessBuffer(buffer); // 直接操作底層數組,無需複製

搜索和擴展

var list = new PooledList<string> { "apple", "banana", "cherry" };
bool found = list.TryFind(s => s.StartsWith("b"), out string result);
Console.WriteLine(found ? result : "Not found"); // 輸出: banana

var pooledFromEnumerable = Enumerable.Range(1, 5).ToPooledList(); // 擴展方法
Console.WriteLine(string.Join(", ", pooledFromEnumerable)); // 輸出: 1, 2, 3, 4, 5

pooledFromEnumerable.Dispose();
  • TryFindTryFindLast:返回 boolout 值,避免 null 檢查。

注意事項與最佳實踐

必須調用 Dispose()

  • 否則不會歸還數組,導致內存泄漏。
  • 推薦 using 塊。

不要長期持有 Span/Memory

  • 因為數組歸還池後可能被其他線程複用。

適用場景

  • 高頻率、大數據臨時集合。
  • 網絡協議解析、日誌聚合、臨時緩存。
  • 需要 Span 訪問的場景。

不適合場景

  • 長期持有的全局集合。
  • 數據量小且生命週期長的普通集合。
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.