<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          .NET性能優(yōu)化-推薦使用Collections.Pooled

          共 7398字,需瀏覽 15分鐘

           ·

          2022-06-01 14:42

          簡介

          性能優(yōu)化就是如何在保證處理相同數(shù)量的請求情況下占用更少的資源,而這個資源一般就是CPU或者內(nèi)存,當(dāng)然還有操作系統(tǒng)IO句柄、網(wǎng)絡(luò)流量、磁盤占用等等。但是絕大多數(shù)時候,我們就是在降低CPU和內(nèi)存的占用率。
          之前分享的內(nèi)容都有一些局限性,很難直接改造,今天要和大家分享一個簡單的方法,只需要替換幾個集合類型,就可以達到提升性能和降低內(nèi)存占用的效果。
          今天要給大家分享一個類庫,這個類庫叫Collections.Pooled,從名字就可以看出來,它是通過池化內(nèi)存來達到降低內(nèi)存占用和GC的目的,后面我們會直接來看看它的性能到底怎么樣,另外也會帶大家看看源碼,為什么它會帶來這些性能提升。

          Collections.Pooled

          項目鏈接:https://github.com/jtmueller/Collections.Pooled
          該庫基于System.Collections.Generic中的類,這些類已經(jīng)被修改,以利用新的System.SpanSystem.Buffers.ArrayPool類庫,達到減少內(nèi)存分配,提高性能,并允許與現(xiàn)代API的更大的互操作性的目的。
          Collections.Pooled支持.NETStandard2.0(.NET Framework 4.6.1+),以及針對.NET Core 2.1+的優(yōu)化構(gòu)建。一套廣泛的單元測試和基準(zhǔn)已經(jīng)從corefx移植過來。

          測試總數(shù):27501。通過:27501。失敗:0。跳過:0。
          測試運行成功。
          測試執(zhí)行時間:9.9019秒

          如何使用

          通過Nuget就可以很簡單的安裝這個類庫,NuGet Version 。

          Install-Package Collections.Pooled
          dotnet add package Collections.Pooled
          paket add Collections.Pooled

          Collections.Pooled類庫中,它針對我們常使用的集合類型都實現(xiàn)了池化的版本,和.NET原生類型的對比如下所示。

          .NET原生Collections.Pooled備注
          ListPooledList泛型集合類
          DictionaryPooledDictionary泛型字典類
          HashSetPooledSet泛型哈希集合類
          StackStack泛型棧
          QueuePooledQueue泛型隊列

          在使用時,我們只需要將對應(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類型實現(xiàn)了IDispose接口,它通過Dispose()方法將使用的內(nèi)存歸還到池中,所以我們需要在使用完Pooled集合對象以后調(diào)用它的Dispose()方法。或者可以直接使用using var關(guān)鍵字。

          using Collections.Pooled;

          // 使用using var 會在pooled對象使用完畢后自動釋放
          using var pooledList = new PooledList<int>();
          Console.WriteLine(pooledList.Count);

          // 使用using作用域 作用域結(jié)束以后就會釋放
          using (var pooledDictionary = new PooledDictionary<int, int>())
          {
          Console.WriteLine(pooledDictionary.Count);
          }

          // 手動調(diào)用Dispose方法
          var pooledStack = new PooledStack<int>();
          Console.WriteLine(pooledStack.Count);
          pooledList.Dispose();

          注意:使用Collections.Pooled內(nèi)的集合對象最好需要釋放掉它,不過不釋放也沒有關(guān)系,GC最終會回收它,只是它不能歸還到池中,達不到節(jié)省內(nèi)存的效果了。
          由于它會復(fù)用內(nèi)存空間,在將內(nèi)存空間返回到池中的時候,需要對集合內(nèi)的元素做處理,它提供了一個叫ClearMode的枚舉供使用,定義如下:

          namespace Collections.Pooled
          {
          ///
          /// 這個枚舉允許控制在內(nèi)部數(shù)組返回到ArrayPool時如何處理數(shù)據(jù)。
          /// 數(shù)組返回到ArrayPool時如何處理數(shù)據(jù)。在使用默認選項之外的其他選項之前,請注意了解
          /// 在使用默認值A(chǔ)uto之外的任何其他選項之前,請仔細了解每個選項的作用。
          ///

          public enum ClearMode
          {
          ///
          /// Auto根據(jù)目標(biāo)框架有不同的行為
          /// .NET Core 2.1: 引用類型和包含引用類型的值類型在內(nèi)部數(shù)組返回池時被清除。不包含引用類型的值類型在返回池時不會被清除。
          /// .NET Standard 2.0: 在返回池之前清除所有用戶類型,以防它們包含引用類型。對于 .NET Standard,Auto 和 Always 具有相同的行為。
          ///

          Auto = 0,

          ///
          /// The Always 設(shè)置的效果是在返回池之前總是清除用戶類型。
          ///

          Always = 1,

          ///
          /// Never 將導(dǎo)致池化集合在將它們返回池之前永遠不會清除用戶類型。
          ///

          Never = 2
          }
          }

          默認情況下,使用默認值A(chǔ)uto即可,如果有特殊的性能要求,知曉風(fēng)險后可以使用Never。
          對于引用類型和包含引用類型的值類型,我們必須在將內(nèi)存空間歸還到池的時候清空數(shù)組引用,如果不清除會導(dǎo)致GC無法釋放這部分內(nèi)存空間(因為元素的引用一直被池持有),如果是純值類型,那么就可以不清空,在使用結(jié)構(gòu)體替代類這篇文章中,我描述了引用類型和結(jié)構(gòu)體(值類型)數(shù)組的存儲區(qū)別,純值類型沒有對象頭回收也無需GC介入。

          性能對比

          我沒有單獨做Benchmark,直接使用的開源項目的跑分結(jié)果,很多項目的內(nèi)存占用都是0,那是因為使用的池化的內(nèi)存,沒有多余的分配

          PooledList

          在Benchmark中循環(huán)向集合添加2048個元素,.NET原生的List需要110us(根據(jù)實際跑分結(jié)果,圖中的毫秒應(yīng)該是筆誤)和263KB內(nèi)存,而PooledList只需要36us0KB內(nèi)存。

          PooledDictionary

          在Benchmark中循環(huán)向字典添加10_0000個元素,.NET原生的Dictionary需要11ms13MB內(nèi)存,而PooledDictionary只需要7ms0MB內(nèi)存。

          PooledSet

          在Benchmark中循環(huán)向哈希集合添加10_0000個元素,.NET原生的HashSet需要5348ms2MB,而PooledSet只需要4723ms0MB內(nèi)存。

          PooledStack

          在Benchmark中循環(huán)向棧添加10_0000個元素,.NET原生的PooledStack需要1079ms2MB,而PooledStack只需要633ms0MB內(nèi)存。

          PooledQueue

          在Benchmark中循環(huán)向隊列添加10_0000個元素,.NET原生的PooledQueue需要681ms1MB,而PooledQueue只需要408ms0MB內(nèi)存。

          未手動釋放場景

          另外在上文中我們提到了Pooled的集合類型需要釋放,但是不釋放也沒有太大的關(guān)系,因為GC會去回收。

          private static readonly string[] List = Enumerable  
          .Range(0, 10000).Select(c => c.ToString()).ToArray();
          // 使用默認的集合類型
          [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 并且及時釋放
          [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é)論。

          • 及時釋放Pooled類型集合幾乎不會觸發(fā)GC和分配內(nèi)存,從上圖中它只分配了56Byte內(nèi)存。

          • 就算不釋放Pooled類型集合,因為它從池中分配內(nèi)存,在進行ReSize擴容操作時還是會復(fù)用內(nèi)存,另外跳過了GC分配內(nèi)存初始化步驟,速度也比較快。

          • 最慢的就是使用普通集合類型,每次ReSize擴容操作都需要申請新的內(nèi)存空間,GC也要回收之前的內(nèi)存空間。

          原理解析

          如果大家看過我之前的博文你應(yīng)該為集合類型設(shè)置初始大小和淺析C# Dictionary實現(xiàn)原理就可以知道,.NET BCL開發(fā)人員為了高性能的隨機訪問,這些基本集合類型的底層數(shù)據(jù)結(jié)構(gòu)都是數(shù)組,我們以List為例。

          • 創(chuàng)建新的數(shù)組來存儲添加進來的元素。

          • 如果數(shù)組空間不夠,那么就觸發(fā)擴容操作,申請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)存,只需要把類庫中使用new關(guān)鍵字申請的地方,改為使用池化的申請。這里和大家分享.NET BCL中的一個類型,叫ArrayPool,它提供了可重復(fù)使用的泛型實例的數(shù)組資源池,使用它可以降低對GC的壓力,在頻繁創(chuàng)建和銷毀數(shù)組的情況下提升性能。
          而我們Pooled類型的底層就是使用ArrayPool來共享資源池,從它的構(gòu)造函數(shù)中,我們可以看到它默認使用的是ArrayPool.Shared來分配數(shù)組對象,當(dāng)然你也可以創(chuàng)建自己的ArrayPool來讓它使用。

          // 默認使用ArrayPool.Shared池
          public PooledList(int capacity, ClearMode clearMode, bool sizeToCapacity) : this(capacity, clearMode, ArrayPool.Shared, sizeToCapacity) { }

          // 分配數(shù)組使用 ArrayPool
          public PooledList(int capacity, ClearMode clearMode, ArrayPool customPool, bool sizeToCapacity)
          {
          if (capacity < 0)
          ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);
          _pool = customPool ?? ArrayPool.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);
          }
          }
          }

          另外在進行容量調(diào)整操作(擴容)時,會將舊的數(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可能會拋出異常,我們直接吞掉
          }
          _items = s_emptyArray;
          }

          另外作者使用了Span優(yōu)化了AddInsert等等API,讓它們有更好的隨機訪問性能;另外還加入了TryXXX系列API,可以更方便的方式的使用它。比如List類相比PooledList就有多達170個修改。

          總結(jié)

          在我們線上實際的使用過程中,完全可以用Pooled提供的集合類型替代原生的集合類型,對降低內(nèi)存占用率和P95延時有非常大的幫助。

          另外就算忘記釋放了,那性能也不會比使用原生的集合類型差多少。當(dāng)然最好的習(xí)慣就是及時的釋放它。


          瀏覽 46
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日韩AV电影免费观看 | 日本尤物在线免费观看 | 天堂国产一区二区三区 | 免费特级黄色片 | 色大香蕉视频 |