.NET使用Collections.Pooled提升性能優(yōu)化的方法
簡(jiǎn)介
性能優(yōu)化就是如何在保證處理相同數(shù)量的請(qǐng)求情況下占用更少的資源,而這個(gè)資源一般就是CPU或者內(nèi)存,當(dāng)然還有操作系統(tǒng)IO句柄、網(wǎng)絡(luò)流量、磁盤占用等等。但是絕大多數(shù)時(shí)候,我們就是在降低CPU和內(nèi)存的占用率。
之前分享的內(nèi)容都有一些局限性,很難直接改造,今天要和大家分享一個(gè)簡(jiǎn)單的方法,只需要替換幾個(gè)集合類型,就可以達(dá)到提升性能和降低內(nèi)存占用的效果。
今天要給大家分享一個(gè)類庫(kù),這個(gè)類庫(kù)叫Collections.Pooled
,從名字就可以看出來(lái),它是通過(guò)池化內(nèi)存來(lái)達(dá)到降低內(nèi)存占用和GC的目的,后面我們會(huì)直接來(lái)看看它的性能到底怎么樣,另外也會(huì)帶大家看看源碼,為什么它會(huì)帶來(lái)這些性能提升。
Collections.Pooled
項(xiàng)目鏈接:https://github.com/jtmueller/Collections.Pooled
該庫(kù)基于System.Collections.Generic
中的類,這些類已經(jīng)被修改,以利用新的System.Span<T>
和System.Buffers.ArrayPool<T>
類庫(kù),達(dá)到減少內(nèi)存分配,提高性能,并允許與現(xiàn)代API的更大的互操作性的目的。Collections.Pooled
支持.NETStandard2.0
(.NET Framework 4.6.1+),以及針對(duì).NET Core 2.1+的優(yōu)化構(gòu)建。一套廣泛的單元測(cè)試和基準(zhǔn)已經(jīng)從corefx移植過(guò)來(lái)。
測(cè)試總數(shù):27501。通過(guò):27501。失?。?。跳過(guò):0。
測(cè)試運(yùn)行成功。
測(cè)試執(zhí)行時(shí)間:9.9019秒
如何使用
通過(guò)Nuget就可以很簡(jiǎn)單的安裝這個(gè)類庫(kù),NuGet Version 。
Install-Package Collections.Pooled dotnet add package Collections.Pooled paket add Collections.Pooled
在Collections.Pooled
類庫(kù)中,它針對(duì)我們常使用的集合類型都實(shí)現(xiàn)了池化的版本,和.NET原生類型的對(duì)比如下所示。
.NET原生 | Collections.Pooled | 備注 |
---|---|---|
List<T> | PooledList<T> | 泛型集合類 |
Dictionary<TKey, TValue> | PooledDictionary<TKey, TValue> | 泛型字典類 |
HashSet<T> | PooledSet<T> | 泛型哈希集合類 |
Stack<T> | Stack<T> | 泛型棧 |
Queue<T> | PooledQueue<T> | 泛型隊(duì)列 |
在使用時(shí),我們只需要將對(duì)應(yīng)的.NET原生版本換成Collections.Pooled
版本就可以了,如下方的代碼所示:
using Collections.Pooled; // 使用方式是一樣的 var list = new List<int>(); var pooledList = new PooledList<int>(); var dictionary = new Dictionary<int,int>(); var pooledDictionary = new PooledDictionary<int,int>(); // 包括PooledSet、PooledQueue、PooledStack的使用方法都是一樣的 var pooledList1 = Enumerable.Range(0,100).ToPooledList(); var pooledDictionary1 = Enumerable.Range(0,100).ToPooledDictionary(i => i, i => i);
但是我們需要注意,Pooled
類型實(shí)現(xiàn)了IDispose
接口,它通過(guò)Dispose()
方法將使用的內(nèi)存歸還到池中,所以我們需要在使用完Pooled
集合對(duì)象以后調(diào)用它的Dispose()
方法?;蛘呖梢灾苯邮褂?code>using var關(guān)鍵字。
using Collections.Pooled; // 使用using var 會(huì)在pooled對(duì)象使用完畢后自動(dòng)釋放 using var pooledList = new PooledList<int>(); Console.WriteLine(pooledList.Count); // 使用using作用域 作用域結(jié)束以后就會(huì)釋放 using (var pooledDictionary = new PooledDictionary<int, int>()) { Console.WriteLine(pooledDictionary.Count); } // 手動(dòng)調(diào)用Dispose方法 var pooledStack = new PooledStack<int>(); Console.WriteLine(pooledStack.Count); pooledList.Dispose();
注意:使用Collections.Pooled內(nèi)的集合對(duì)象最好需要釋放掉它,不過(guò)不釋放也沒(méi)有關(guān)系,GC最終會(huì)回收它,只是它不能歸還到池中,達(dá)不到節(jié)省內(nèi)存的效果了。
由于它會(huì)復(fù)用內(nèi)存空間,在將內(nèi)存空間返回到池中的時(shí)候,需要對(duì)集合內(nèi)的元素做處理,它提供了一個(gè)叫ClearMode
的枚舉供使用,定義如下:
namespace Collections.Pooled { /// <summary> /// 這個(gè)枚舉允許控制在內(nèi)部數(shù)組返回到ArrayPool時(shí)如何處理數(shù)據(jù)。 /// 數(shù)組返回到ArrayPool時(shí)如何處理數(shù)據(jù)。在使用默認(rèn)選項(xiàng)之外的其他選項(xiàng)之前,請(qǐng)注意了解 /// 在使用默認(rèn)值A(chǔ)uto之外的任何其他選項(xiàng)之前,請(qǐng)仔細(xì)了解每個(gè)選項(xiàng)的作用。 /// </summary> public enum ClearMode { /// <summary> /// <para><code>Auto</code>根據(jù)目標(biāo)框架有不同的行為</para> /// <para>.NET Core 2.1: 引用類型和包含引用類型的值類型在內(nèi)部數(shù)組返回池時(shí)被清除。 不包含引用類型的值類型在返回池時(shí)不會(huì)被清除。</para> /// <para>.NET Standard 2.0: 在返回池之前清除所有用戶類型,以防它們包含引用類型。 對(duì)于 .NET Standard,Auto 和 Always 具有相同的行為。</para> /// </summary> Auto = 0, /// <summary> /// The <para><code>Always</code> 設(shè)置的效果是在返回池之前總是清除用戶類型。 /// </summary> Always = 1, /// <summary> /// <para><code>Never</code> 將導(dǎo)致池化集合在將它們返回池之前永遠(yuǎn)不會(huì)清除用戶類型。</para> /// </summary> Never = 2 } }
默認(rèn)情況下,使用默認(rèn)值A(chǔ)uto即可,如果有特殊的性能要求,知曉風(fēng)險(xiǎn)后可以使用Never。
對(duì)于引用類型和包含引用類型的值類型,我們必須在將內(nèi)存空間歸還到池的時(shí)候清空數(shù)組引用,如果不清除會(huì)導(dǎo)致GC無(wú)法釋放這部分內(nèi)存空間(因?yàn)樵氐囊靡恢北怀爻钟校绻羌冎殿愋?,那么就可以不清空,在使用結(jié)構(gòu)體替代類這篇文章中,我描述了引用類型和結(jié)構(gòu)體(值類型)數(shù)組的存儲(chǔ)區(qū)別,純值類型沒(méi)有對(duì)象頭回收也無(wú)需GC介入。
性能對(duì)比
我沒(méi)有單獨(dú)做Benchmark,直接使用的開(kāi)源項(xiàng)目的跑分結(jié)果,很多項(xiàng)目的內(nèi)存占用都是0,那是因?yàn)槭褂玫某鼗膬?nèi)存,沒(méi)有多余的分配。
PooledList<T>
在Benchmark中循環(huán)向集合添加2048個(gè)元素,.NET原生的List<T>
需要110us(根據(jù)實(shí)際跑分結(jié)果,圖中的毫秒應(yīng)該是筆誤)和263KB內(nèi)存,而PooledList<T>
只需要36us和0KB內(nèi)存。
PooledDictionary<TKey, TValue>
在Benchmark中循環(huán)向字典添加10_0000個(gè)元素,.NET原生的Dictionary<TKey, TValue>
需要11ms和13MB內(nèi)存,而PooledDictionary<TKey, TValue>
只需要7ms和0MB內(nèi)存。
PooledSet<T>
在Benchmark中循環(huán)向哈希集合添加10_0000個(gè)元素,.NET原生的HashSet<T>
需要5348ms和2MB,而PooledSet<T>
只需要4723ms和0MB內(nèi)存。
PooledStack<T>
在Benchmark中循環(huán)向棧添加10_0000個(gè)元素,.NET原生的PooledStack<T>
需要1079ms和2MB,而PooledStack<T>
只需要633ms和0MB內(nèi)存。
PooledQueue<T>
在Benchmark中循環(huán)向隊(duì)列添加10_0000個(gè)元素,.NET原生的PooledQueue<T>
需要681ms和1MB,而PooledQueue<T>
只需要408ms和0MB內(nèi)存。
未手動(dòng)釋放場(chǎng)景
另外在上文中我們提到了Pooled
的集合類型需要釋放,但是不釋放也沒(méi)有太大的關(guān)系,因?yàn)镚C會(huì)去回收。
private static readonly string[] List = Enumerable .Range(0, 10000).Select(c => c.ToString()).ToArray(); // 使用默認(rèn)的集合類型 [Benchmark(Baseline = true)] public int UseList() { var list = new List<string>(1024); for (var index = 0; index < List.Length; index++) { var item = List[index]; list.Add(item); } return list.Count; } // 使用PooledList 并且及時(shí)釋放 [Benchmark] public int UsePooled() { using var list = new PooledList<string>(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 PooledList<string>(1024); for (var index = 0; index < List.Length; index++) { var item = List[index]; list.Add(item); } return list.Count; }
Benchmark結(jié)果如下:
可以從上面的Benchmark結(jié)果可以得出結(jié)論。
- 及時(shí)釋放
Pooled
類型集合幾乎不會(huì)觸發(fā)GC和分配內(nèi)存,從上圖中它只分配了56Byte內(nèi)存。 - 就算不釋放
Pooled
類型集合,因?yàn)樗鼜某刂蟹峙鋬?nèi)存,在進(jìn)行ReSize
擴(kuò)容操作時(shí)還是會(huì)復(fù)用內(nèi)存,另外跳過(guò)了GC分配內(nèi)存初始化步驟,速度也比較快。 - 最慢的就是使用普通集合類型,每次
ReSize
擴(kuò)容操作都需要申請(qǐng)新的內(nèi)存空間,GC也要回收之前的內(nèi)存空間。
原理解析
如果大家看過(guò)我之前的博文你應(yīng)該為集合類型設(shè)置初始大小和淺析C# Dictionary實(shí)現(xiàn)原理就可以知道,.NET BCL開(kāi)發(fā)人員為了高性能的隨機(jī)訪問(wèn),這些基本集合類型的底層數(shù)據(jù)結(jié)構(gòu)都是數(shù)組,我們以List<T>
為例。
- 創(chuàng)建新的數(shù)組來(lái)存儲(chǔ)添加進(jìn)來(lái)的元素。
- 如果數(shù)組空間不夠,那么就觸發(fā)擴(kuò)容操作,申請(qǐng)2倍的空間大小。
構(gòu)造函數(shù)代碼如下,可以看到是直接創(chuàng)建的泛型數(shù)組:
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]; }
那么如果想要池化內(nèi)存,只需要把類庫(kù)中使用new
關(guān)鍵字申請(qǐng)的地方,改為使用池化的申請(qǐng)。這里和大家分享.NET BCL中的一個(gè)類型,叫ArrayPool
,它提供了可重復(fù)使用的泛型實(shí)例的數(shù)組資源池,使用它可以降低對(duì)GC的壓力,在頻繁創(chuàng)建和銷毀數(shù)組的情況下提升性能。
而我們Pooled
類型的底層就是使用ArrayPool
來(lái)共享資源池,從它的構(gòu)造函數(shù)中,我們可以看到它默認(rèn)使用的是ArrayPool<T>.Shared
來(lái)分配數(shù)組對(duì)象,當(dāng)然你也可以創(chuàng)建自己的ArrayPool
來(lái)讓它使用。
// 默認(rèn)使用ArrayPool<T>.Shared池 public PooledList(int capacity, ClearMode clearMode, bool sizeToCapacity) : this(capacity, clearMode, ArrayPool<T>.Shared, sizeToCapacity) { } // 分配數(shù)組使用 ArrayPool public PooledList(int capacity, ClearMode clearMode, ArrayPool<T> customPool, bool sizeToCapacity) { if (capacity < 0) ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); _pool = customPool ?? ArrayPool<T>.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); } } }
另外在進(jìn)行容量調(diào)整操作(擴(kuò)容)時(shí),會(huì)將舊的數(shù)組歸還回線程池,新的數(shù)組也在池中獲取。
public int Capacity { get => _items.Length; set { if (value < _size) { ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.value, ExceptionResource.ArgumentOutOfRange_SmallCapacity); } if (value != _items.Length) { if (value > 0) { // 從池中分配數(shù)組 var newItems = _pool.Rent(value); if (_size > 0) { Array.Copy(_items, newItems, _size); } // 舊數(shù)組歸還到池中 ReturnArray(); _items = newItems; } else { ReturnArray(); _size = 0; } } } } private void ReturnArray() { if (_items.Length == 0) return; try { // 歸還到池中 _pool.Return(_items, clearArray: _clearOnFree); } catch (ArgumentException) { // ArrayPool可能會(huì)拋出異常,我們直接吞掉 } _items = s_emptyArray; }
另外作者使用了Span優(yōu)化了Add
、Insert
等等API,讓它們有更好的隨機(jī)訪問(wèn)性能;另外還加入了TryXXX
系列API,可以更方便的方式的使用它。比如List<T>
類相比PooledList<T>
就有多達(dá)170個(gè)修改。
總結(jié)
在我們線上實(shí)際的使用過(guò)程中,完全可以用Pooled
提供的集合類型替代原生的集合類型,對(duì)降低內(nèi)存占用率和P95延時(shí)有非常大的幫助。
另外就算忘記釋放了,那性能也不會(huì)比使用原生的集合類型差多少。當(dāng)然最好的習(xí)慣就是及時(shí)的釋放它。
到此這篇關(guān)于.NET使用Collections.Pooled性能優(yōu)化的方法的文章就介紹到這了,更多相關(guān).net性能優(yōu)化內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
EFCore 通過(guò)實(shí)體Model生成創(chuàng)建SQL Server數(shù)據(jù)庫(kù)表腳本
這篇文章主要介紹了EFCore 通過(guò)實(shí)體Model生成創(chuàng)建SQL Server數(shù)據(jù)庫(kù)表腳本的示例,幫助大家更好的理解和學(xué)習(xí)使用.net框架,感興趣的朋友可以了解下2021-03-03asp.net中使用 Repeater控件拖拽實(shí)現(xiàn)排序并同步數(shù)據(jù)庫(kù)字段排序
這篇文章主要介紹了asp.net中使用 Repeater控件拖拽實(shí)現(xiàn)排序并同步數(shù)據(jù)庫(kù)字段排序的相關(guān)資料,需要的朋友可以參考下2015-12-12.NET Core之微信支付之公眾號(hào)、H5支付詳解
這篇文章主要介紹了.NET Core之微信支付之公眾號(hào)、H5支付篇,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03詳解免費(fèi)開(kāi)源的.NET多類型文件解壓縮組件SharpZipLib(.NET組件介紹之七)
本篇文章主要介紹了免費(fèi)開(kāi)源的.NET多類型文件解壓縮組件SharpZipLib,這也是一種解壓縮組件,具有一定的參考價(jià)值,有興趣的可以了解一下。2016-12-12ASP.NET比較常用的26個(gè)性能優(yōu)化技巧
這篇文章主要給大家介紹asp.net中比較常用的26個(gè)性能優(yōu)化技巧,主要設(shè)計(jì)到asp.net中常用的26個(gè)性能優(yōu)化方面的內(nèi)容,對(duì)于asp.net中常用的26個(gè)性能優(yōu)化技巧感興趣的朋友可以參考下本篇文章2015-10-10Asp.Net各種超時(shí)問(wèn)題總結(jié)
在數(shù)據(jù)庫(kù)或者請(qǐng)求操作時(shí),如果選擇的時(shí)間段過(guò)短或操作數(shù)據(jù)量過(guò)大,就會(huì)遇到"請(qǐng)求超時(shí)"的的問(wèn)題,網(wǎng)絡(luò)上提供很多解決方案,但普遍不完善,根據(jù)個(gè)人經(jīng)驗(yàn)及參考網(wǎng)絡(luò)解決方案,先將其匯總2013-02-02