C#性能对比:每次新建List还是使用List对象池更优?
C#中两种List实现的性能对比分析
问题背景
在名为MyStaticClass的类中,以下两种实现哪种性能更优?第二种通过对象池复用List<SomeClass>,避免频繁创建新实例,但每次复用前会调用Clear(),这种方式是否能提升性能,还是会因Clear()失去优化价值?
第一种实现:每次新建List
void FunctionThatGetsCalledAlot(...) { List<SomeClass> previousAttributes = new List<SomeClass>(); previousAttributes.AddRange(SomeStuff); node.RemoveAttributes(); // 其他逻辑 }
第二种实现:基于对象池复用List
private class ThreadSafeObjectPool<T> where T : new() { private readonly Stack<T> pool = new Stack<T>(); public T Get() { lock (this.pool) { if (this.pool.Count > 0) { return this.pool.Pop(); } } return new T(); } public void Return(T v) { lock (this.pool) { this.pool.Push(v); } } } private static readonly ThreadSafeObjectPool<List<SomeClass>> previousAttributesPool = new ThreadSafeObjectPool<List<SomeClass>>(); void FunctionThatGetsCalledAlot(...) { List<SomeClass> previousAttributes = MyStaticClass.previousAttributesPool.Get(); try { previousAttributes.Clear(); previousAttributes.AddRange(SomeStuff); // 其他逻辑 } finally { MyStaticClass.previousAttributesPool.Return(previousAttributes); } }
注:修正了原代码中对象池实例化的拼写错误(ObjectPool改为ThreadSafeObjectPool,SomeCLass改为SomeClass)
性能分析
核心差异:List.Clear()的行为
首先要明确:List<T>.Clear()不会释放底层的数组内存,它仅重置List内部的元素计数(_size字段),底层用于存储元素的数组容量保持不变。这是第二种实现能获得性能优势的关键。
两种实现的开销对比
第一种实现的开销
- 每次调用都会创建新的
List<SomeClass>实例,默认初始容量为4。如果SomeStuff的元素数量超过当前容量,List会触发多次数组扩容(每次容量翻倍),伴随内存分配和元素拷贝的开销。 - 使用后的List会成为短生命周期对象,频繁创建和回收会增加GC的压力,尤其是Gen 0垃圾回收的频率,在高调用量场景下会显著影响性能。
- 每次调用都会创建新的
第二种实现的优势与额外开销
- 优势:复用List实例时,底层数组的容量被保留。如果
SomeStuff的元素数量相对稳定(或不会远大于之前的容量),AddRange操作无需重新扩容,节省了内存分配和拷贝的开销;同时减少了短生命周期对象的产生,降低了GC的工作量。 - 额外开销:线程安全的对象池在
Get()和Return()时会加锁。单线程场景下这部分锁开销是不必要的,可以改用非线程安全的对象池;但多线程场景下锁是必须的,不过相比内存分配和GC的开销,高调用量场景下锁的影响通常更小。
- 优势:复用List实例时,底层数组的容量被保留。如果
结论
在FunctionThatGetsCalledAlot被频繁调用的场景下,第二种实现通常能提升性能——Clear()并没有失去优化价值,它保留了List的底层数组容量,避免了重复的扩容和内存分配,同时减轻了GC压力。
但也存在例外情况:
- 如果
SomeStuff的元素数量波动极大(每次都远大于List的现有容量,导致每次复用都需要扩容),第二种的优势会被削弱。 - 如果调用频率很低,第一种实现的简单性反而更优,锁的开销可能超过对象池带来的收益。
内容的提问来源于stack exchange,提问作者NickLokarno
相关产品推荐
相关产品推荐

