.NET性能優化-使用RecyclableMemoryStream替代MemoryStream

 

提到 MemoryStream 大家可能都不陌生,在編寫代碼中或多或少有使用過;比如Json序列化反序列化、導出PDF/Excel/Word、進行圖片或者文字處理等場景。但是如果使用它高頻、大數據量處理這些數據,就存在一些性能陷阱。

今天給大家帶來的這個優化技巧其實就是池化 MemoryStream 的版本 RecyclableMemoryStream ,它通過池化 MemoryStream 底層buffer來 降低內存佔用率、GC暫停時間和GC次數達到提升性能目的。

它的開源庫地址如下鏈接:

https://github.com/microsoft/Microsoft.IO.RecyclableMemoryStream

使用它也非常簡單,直接安裝對應的Nuget包即可,目前最新版本是 2.2.1 版本。

// 命令行安裝 dotnet addpackage Microsoft.IO.RecyclableMemoryStream --version2.2.1 // csproj 安裝 <PackageReference Include="Microsoft.IO.RecyclableMemoryStream "Version="2.2.1 "/>

然後創建一個 RecyclableMemoryStreamManager 對象,即可使用它的 GetStream 方法來獲取一個池化的流,當然使用完這個流以後需要調用 Dispose 方法將其歸還到池中,也可以使用 using 模式來釋放。

classProgram{privatestaticreadonlyRecyclableMemoryStreamManagermanager =newRecyclableMemoryStreamManager();staticvoidMain(string[]args ){varsourceBuffer =newbyte[]{0,1,2,3,4,5,6,7};using(varstream =manager .GetStream()){stream .Write(sourceBuffer ,0,sourceBuffer .Length );}}}

在創建 RecyclableMemoryStreamManager 和 GetStream 時有很多選項,可以設置底層buffer的大小、爲流進行命名隔離等精細化的選項,這些大家可以看官方文檔瞭解,本文不再贅述。

性能比較

爲了直觀的比較性能,我構建了一個Benchmark,這個基準測試分別使用 MemoryStream 和 RecyclableMemoryStream 實現數據緩衝的功能,下面是測試代碼:

publicclassBenchmarkRecyclableMemoryStream{// 生成隨機數privatestaticreadonlyRandomRandom =new(1024);// 填充的數據privatestaticreadonlybyte[]Data =Enumerable .Range(0,81920).Select(d =>(byte)d ).ToArray();// 每次隨機填充privatestaticreadonlyint[]DataLength =Enumerable .Range(0,1000).Select(d =>Random .Next(10240,81920)).ToArray();// RecyclableManagerprivatestaticreadonlyRecyclableMemoryStreamManagerManager =new();[Benchmark(Baseline =true)]publiclongUseMemoryStream(){varsum =0L;for(inti =0;i <DataLength .Length ;i ++){usingvarstream =newMemoryStream();stream .Write(Data ,0,DataLength [i ]);sum +=stream .Length ;}returnsum ;}[Benchmark]publiclongUseRecyclableMemoryStream(){varsum =0L;for(inti =0;i <DataLength .Length ;i ++){usingvarstream =Manager .GetStream();stream .Write(Data ,0,DataLength [i ]);sum +=stream .Length ;}returnsum ;}}

下方是測試的結果,可以看到使用 RecyclableMemoryStream 比直接使用 MemoryStream 在內存和速度上有很大的優勢。

  • 執行效率快 51%
  • 內存分配要低 99.4%
工作原理

RecyclableMemoryStream 提升GC性能的方式是通過將緩衝區分配和保持在第二代堆,這能減少FullGC的頻率,另外如果您設置的緩衝區大小超過85,000字節,那麼緩衝區將分配在LOH上,GC不會經常掃描這些對象堆。

RecyclableMemoryStreamManager 類維護了兩個獨立的對象池:

  • 小型池 :保存小型緩衝區(可配置大小),默認情況下用於所有正常的讀、寫操作,多個小的緩衝區能鏈接在一起,形成單獨的 Stream 。
  • 大型池 :保存大型緩衝區,只有在必須需要單個且連續緩衝區才使用,比如調用 GetBuffer 方法,它可以創建比單個緩衝區大的多的 Stream ,最大不超過.NET對數組類型的限制。

RecyclableMemoryStream 首先會使用一個小的緩衝區,隨着寫入數據的增多,會將其它緩衝區鏈接起來組合使用。如果您調用了 GetBuffer 方法,並且已有的數據大於單個小緩衝區的容量,那麼就會被轉換爲大緩衝區。

另外您還可以爲 Stream 設置初始容量,如果容量大於單個緩衝區大小,會在一開始就鏈接好多個塊,當然也可以直接分配大型緩衝區,只需將 asContiguousBuffer 設置爲true。

大型池有兩個版本:

  • 線性 (默認):指定一個倍數和最大的大小,然後創建一個緩衝區數組,從(1x倍數)、(2x倍數)一直到最大值。
  • 指數 :緩衝區不是線性增長而是指數增長,每個槽大小將增加一倍。
如下圖所示:

那麼您應該用哪一個?這取決於您的業務場景。如果您的緩衝區大小不可預測,那麼線性緩衝區可能更合適。如果您知道不可能分配較長的流長度,但是可能有很多較小尺寸的流,那麼選擇指數版本可能會導致較少的總體內存使用。

緩衝區是在第一次被請求時按需創建的。使用完 Stream 後,這些緩衝區將通過 RecyclableMemoryStream 的 Dispose 方法返回到池中。當這種返回發生時, RecyclableMemoryStreamManager 將使用屬性 MaximumFreeSmallPoolBytes 和 MaximumFreeLargePoolBytes 來決定是否將這些緩衝區放回池中,或者讓它們離開(從而被垃圾收集)。正是通過這些屬性,你決定了你的池子可以增長到多大。如果你把這些屬性設置爲0,你就會有無限制的池增長,這與內存泄漏基本上沒有區別。對於每一個應用程序,你必須通過分析和實驗來確定內存池大小和垃圾收集之間的適當平衡。

如果忘記調用流的 Dispose 方法,可能會導致內存泄漏。爲了幫助您避免這種情況,每個流都有一個終結器,一旦沒有更多對流的引用,CLR 將調用該終結器。此終結器將引發有關泄漏流的事件或記錄有關泄漏流的消息。

請注意,由於性能原因,緩衝區從來沒有預先初始化或歸零。您有責任確保它們的內容是有效和安全的,可以使用緩衝區回收。

使用指南

雖然這個庫力求非常通用化,並且不會對如何使用它施加太多限制,但是它的目的是減少由於頻繁的大量分配而產生的垃圾收集的成本。因此,以下是一些對你有用的通用使用指南:

  1. 將 blockSize 、 largeBufferMultiple 、 maxBufferSize 、 MaximumFreeLargePoolBytes 和 MaximumFreeSmallPoolBytes 屬性設置爲符合你的應用和資源要求的合理值。如果你不設置 MaximumFreeLargePoolBytes 和 MaximumFreeSmallPoolBytes ,就有可能出現無限制的內存增長!
  2. 每個流總是被精確地 Dispose 一次。
  3. 大多數應用程序不應該調用 ToArray ,如果可能,應該避免調用 GetBuffer 。相反,使用 GetReadOnlySequence 來讀取,使用 IBufferWriter 方法 GetSpan 、 GetMemory 和 Advance 來寫入。還有一些雜七雜八的 CopyTo 和 WriteTo 方法,可能很方便。重點是要儘可能避免產生不必要的GC壓力。
  4. 通過實驗找到適合你情況的設置。

在你嘗試用這個庫來優化你的方案之前,對垃圾收集器有一定的瞭解是一個非常好的主意。像垃圾收集這樣的文章,或者像《編寫高性能的.NET代碼》這樣的書,將幫助你理解這個庫的設計原則。

在配置選項時,要考慮這樣的問題。

  • 我期望的流的長度分佈是怎樣的?
  • 有多少個流會在同一時間被使用?
  • GetBuffer 是否經常被調用?我需要多大程度的使用大型池緩衝區?
  • 我需要對活動高峯有多大的彈性?即我應該保留多少空閒字節以備不時之需?
  • 我在要使用的機器上有哪些物理內存限制?
總結

本文中介紹了一個通用的 MemoryStream 池化庫,使用它能顯著的提升你係統的性能,你幾乎可以在任何場景使用 RecyclableMemoryStream 替代 MemoryStream 。要知道在我們性能評測中, RecyclableMemoryStream 比 MemoryStream 快51%,而且它能節省99.4%的內存分配。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章