新手做那些网站比较好,儿童设计网站,wordpress 进入,广州注册公司流程及资料简介性能优化就是如何在保证处理相同数量的请求情况下占用更少的资源#xff0c;而这个资源一般就是CPU或者内存#xff0c;当然还有操作系统IO句柄、网络流量、磁盘占用等等。但是绝大多数时候#xff0c;我们就是在降低CPU和内存的占用率。之前分享的内容都有一些局限性而这个资源一般就是CPU或者内存当然还有操作系统IO句柄、网络流量、磁盘占用等等。但是绝大多数时候我们就是在降低CPU和内存的占用率。之前分享的内容都有一些局限性很难直接改造今天要和大家分享一个简单的方法只需要替换几个集合类型就可以达到提升性能和降低内存占用的效果。今天要给大家分享一个类库这个类库叫Collections.Pooled从名字就可以看出来它是通过池化内存来达到降低内存占用和GC的目的后面我们会直接来看看它的性能到底怎么样另外也会带大家看看源码为什么它会带来这些性能提升。Collections.Pooled项目链接https://github.com/jtmueller/Collections.Pooled该库基于System.Collections.Generic中的类这些类已经被修改以利用新的System.SpanT和System.Buffers.ArrayPoolT类库达到减少内存分配提高性能并允许与现代API的更大的互操作性的目的。Collections.Pooled支持.NETStandard2.0.NET Framework 4.6.1以及针对.NET Core 2.1的优化构建。一套广泛的单元测试和基准已经从corefx移植过来。测试总数27501。通过27501。失败0。跳过0。
测试运行成功。
测试执行时间9.9019秒如何使用通过Nuget就可以很简单的安装这个类库NuGet Version 。Install-Package Collections.Pooled
dotnet add package Collections.Pooled
paket add Collections.Pooled在Collections.Pooled类库中它针对我们常使用的集合类型都实现了池化的版本和.NET原生类型的对比如下所示。.NET原生Collections.Pooled备注ListTPooledListT泛型集合类DictionaryTKey, TValuePooledDictionaryTKey, TValue泛型字典类HashSetTPooledSetT泛型哈希集合类StackTStackT泛型栈QueueTPooledQueueT泛型队列在使用时我们只需要将对应的.NET原生版本换成Collections.Pooled版本就可以了如下方的代码所示using Collections.Pooled;// 使用方式是一样的
var list new Listint();
var pooledList new PooledListint();var dictionary new Dictionaryint,int();
var pooledDictionary new PooledDictionaryint,int();// 包括PooledSet、PooledQueue、PooledStack的使用方法都是一样的var pooledList1 Enumerable.Range(0,100).ToPooledList();
var pooledDictionary1 Enumerable.Range(0,100).ToPooledDictionary(i i, i i);但是我们需要注意Pooled类型实现了IDispose接口它通过Dispose()方法将使用的内存归还到池中所以我们需要在使用完Pooled集合对象以后调用它的Dispose()方法。或者可以直接使用using var关键字。using Collections.Pooled;// 使用using var 会在pooled对象使用完毕后自动释放
using var pooledList new PooledListint();
Console.WriteLine(pooledList.Count);// 使用using作用域 作用域结束以后就会释放
using (var pooledDictionary new PooledDictionaryint, int())
{Console.WriteLine(pooledDictionary.Count);
}// 手动调用Dispose方法
var pooledStack new PooledStackint();
Console.WriteLine(pooledStack.Count);
pooledList.Dispose();注意使用Collections.Pooled内的集合对象最好需要释放掉它不过不释放也没有关系GC最终会回收它只是它不能归还到池中达不到节省内存的效果了。由于它会复用内存空间在将内存空间返回到池中的时候需要对集合内的元素做处理它提供了一个叫ClearMode的枚举供使用定义如下namespace Collections.Pooled
{/// summary/// 这个枚举允许控制在内部数组返回到ArrayPool时如何处理数据。/// 数组返回到ArrayPool时如何处理数据。在使用默认选项之外的其他选项之前请注意了解 /// 在使用默认值Auto之外的任何其他选项之前请仔细了解每个选项的作用。/// /summarypublic enum ClearMode{/// summary/// paracodeAuto/code根据目标框架有不同的行为/para/// para.NET Core 2.1: 引用类型和包含引用类型的值类型在内部数组返回池时被清除。不包含引用类型的值类型在返回池时不会被清除。/para/// para.NET Standard 2.0: 在返回池之前清除所有用户类型以防它们包含引用类型。对于 .NET StandardAuto 和 Always 具有相同的行为。/para/// /summaryAuto 0,/// summary/// The paracodeAlways/code 设置的效果是在返回池之前总是清除用户类型。/// /summaryAlways 1,/// summary/// paracodeNever/code 将导致池化集合在将它们返回池之前永远不会清除用户类型。/para/// /summaryNever 2}
}默认情况下使用默认值Auto即可如果有特殊的性能要求知晓风险后可以使用Never。对于引用类型和包含引用类型的值类型我们必须在将内存空间归还到池的时候清空数组引用如果不清除会导致GC无法释放这部分内存空间因为元素的引用一直被池持有如果是纯值类型那么就可以不清空在使用结构体替代类这篇文章中我描述了引用类型和结构体(值类型)数组的存储区别纯值类型没有对象头回收也无需GC介入。性能对比我没有单独做Benchmark直接使用的开源项目的跑分结果很多项目的内存占用都是0那是因为使用的池化的内存没有多余的分配。PooledListT在Benchmark中循环向集合添加2048个元素.NET原生的ListT需要110us(根据实际跑分结果图中的毫秒应该是笔误)和263KB内存而PooledListT只需要36us和0KB内存。PooledDictionaryTKey, TValue在Benchmark中循环向字典添加10_0000个元素.NET原生的DictionaryTKey, TValue需要11ms和13MB内存而PooledDictionaryTKey, TValue只需要7ms和0MB内存。PooledSetT在Benchmark中循环向哈希集合添加10_0000个元素.NET原生的HashSetT需要5348ms和2MB而PooledSetT只需要4723ms和0MB内存。PooledStackT在Benchmark中循环向栈添加10_0000个元素.NET原生的PooledStackT需要1079ms和2MB而PooledStackT只需要633ms和0MB内存。PooledQueueT在Benchmark中循环向队列添加10_0000个元素.NET原生的PooledQueueT需要681ms和1MB而PooledQueueT只需要408ms和0MB内存。未手动释放场景另外在上文中我们提到了Pooled的集合类型需要释放但是不释放也没有太大的关系因为GC会去回收。private static readonly string[] List Enumerable .Range(0, 10000).Select(c c.ToString()).ToArray();
// 使用默认的集合类型
[Benchmark(Baseline true)]
public int UseList()
{ var list new Liststring(1024); for (var index 0; index List.Length; index) { var item List[index]; list.Add(item); } return list.Count;
}
// 使用PooledList 并且及时释放
[Benchmark]
public int UsePooled()
{ using var list new PooledListstring(1024); for (var index 0; index List.Length; index) { var item List[index]; list.Add(item); } return list.Count;
}
// 使用PooledList 不释放
[Benchmark]
public int UsePooledWithOutUsing()
{ var list new PooledListstring(1024); for (var index 0; index List.Length; index) { var item List[index]; list.Add(item); } return list.Count;
}Benchmark结果如下可以从上面的Benchmark结果可以得出结论。及时释放Pooled类型集合几乎不会触发GC和分配内存从上图中它只分配了56Byte内存。就算不释放Pooled类型集合因为它从池中分配内存在进行ReSize扩容操作时还是会复用内存另外跳过了GC分配内存初始化步骤速度也比较快。最慢的就是使用普通集合类型每次ReSize扩容操作都需要申请新的内存空间GC也要回收之前的内存空间。原理解析如果大家看过我之前的博文你应该为集合类型设置初始大小和浅析C# Dictionary实现原理就可以知道.NET BCL开发人员为了高性能的随机访问这些基本集合类型的底层数据结构都是数组我们以ListT为例。创建新的数组来存储添加进来的元素。如果数组空间不够那么就触发扩容操作申请2倍的空间大小。构造函数代码如下可以看到是直接创建的泛型数组public List(int capacity)
{if (capacity 0)ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);if (capacity 0)_items s_emptyArray;else_items new T[capacity];
}那么如果想要池化内存只需要把类库中使用new关键字申请的地方改为使用池化的申请。这里和大家分享.NET BCL中的一个类型叫ArrayPool它提供了可重复使用的泛型实例的数组资源池使用它可以降低对GC的压力在频繁创建和销毁数组的情况下提升性能。而我们Pooled类型的底层就是使用ArrayPool来共享资源池从它的构造函数中我们可以看到它默认使用的是ArrayPoolT.Shared来分配数组对象当然你也可以创建自己的ArrayPool来让它使用。// 默认使用ArrayPoolT.Shared池
public PooledList(int capacity, ClearMode clearMode, bool sizeToCapacity) : this(capacity, clearMode, ArrayPoolT.Shared, sizeToCapacity) { } // 分配数组使用 ArrayPool
public PooledList(int capacity, ClearMode clearMode, ArrayPoolT customPool, bool sizeToCapacity)
{if (capacity 0)ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);_pool customPool ?? ArrayPoolT.Shared;_clearOnFree ShouldClear(clearMode);if (capacity 0){_items s_emptyArray;}else{_items _pool.Rent(capacity);}if (sizeToCapacity){_size capacity;if (clearMode ! ClearMode.Never){Array.Clear(_items, 0, _size);}}}另外在进行容量调整操作扩容时会将旧的数组归还回线程池新的数组也在池中获取。public int Capacity
{get _items.Length;set{if (value _size){ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.value, ExceptionResource.ArgumentOutOfRange_SmallCapacity);}if (value ! _items.Length){if (value 0){// 从池中分配数组var newItems _pool.Rent(value);if (_size 0){Array.Copy(_items, newItems, _size);}// 旧数组归还到池中ReturnArray();_items newItems;}else{ReturnArray();_size 0;}}}
}
private void ReturnArray()
{ if (_items.Length 0) return; try { // 归还到池中_pool.Return(_items, clearArray: _clearOnFree); } catch (ArgumentException) { // ArrayPool可能会抛出异常我们直接吞掉 } _items s_emptyArray;
}另外作者使用了Span优化了Add、Insert等等API让它们有更好的随机访问性能另外还加入了TryXXX系列API可以更方便的方式的使用它。比如ListT类相比PooledListT就有多达170个修改。总结在我们线上实际的使用过程中完全可以用Pooled提供的集合类型替代原生的集合类型对降低内存占用率和P95延时有非常大的帮助。另外就算忘记释放了那性能也不会比使用原生的集合类型差多少。当然最好的习惯就是及时的释放它。