.Net性能調優-ArrayPool


定義

高性能托管數組緩沖池,可重復使用,用租用空間的方式代替重新分配數組空間的行為

好處

可以在頻繁創建和銷毀數組的情況下提高性能,減少垃圾回收器的壓力

使用

  • 獲取緩沖池實例:Create/Shared var pool=ArrayPool[byte].Shared
  • 調用緩沖池實例Rent()函數,租用緩沖區空間 byte[] array=pool.Rent(1024)
  • 調用緩沖池實例Return(array[T])函數,歸還租用的空間 pool.Return(array)

Shared

Shared返回為一個靜態共享實例,實際返回了一個TlsOverPerCoreLockedStacksArrayPool

internal sealed class TlsOverPerCoreLockedStacksArrayPool<T> : ArrayPool<T>
{
    private static readonly TlsOverPerCoreLockedStacksArrayPool<T> s_shared = new TlsOverPerCoreLockedStacksArrayPool<T>();

    public static ArrayPool<T> Shared => s_shared;
}

特點

  • 租用數組長度不可超過 2^20( 1024*1024 = 1 048 576),否則會從GC中重新開辟內存空間
  • Rent租用數組實際返回的長度可能比請求的長度大,返回長度一是(16*2^n)
  • Return歸還緩沖區的時候,如果不設置clearArray,下一個租用者可能會看到之前的填充的值(在返回的數組長度剛好是下一個租用者請求的長度時會被看到)
  • 緩沖池的內存釋放不是實時釋放,在緩沖區空閑時,大概10到20秒之后,會隨着第2代GC一起釋放,分批釋放
  • 並發數量持續增長時,緩沖池占用的內存空間也會持續增長,而且似乎沒有上限

耗時對比

private static void TimeMonitor()
{
    //隨機生成3000個數組的長度值
    var sizes = new int[30000];
    Parallel.For(0, 10000, x => { sizes[x] = new Random().Next(1024 * 800, 1024 * 1024); });

    //緩沖池方式租用數組
    var gcAllocate0 = GC.GetTotalAllocatedBytes();
    var watch = new Stopwatch();
    Console.WriteLine("start");
    watch.Start();
    for (int i = 0; i < 10000; i++)
    {
        //CreateArrayByPool(ArrayPool<int>.Shared, 1024 * 1024,sizes[i], false);

        var arr = ArrayPool<int>.Shared.Rent(sizes[i]);
        for (int j = 0; j < sizes[i]; j++)
        {
            arr[j] = i;
        }
        ArrayPool<int>.Shared.Return(arr, true);
    }
    var time1 = watch.ElapsedMilliseconds;
    var gcAllocate1 = GC.GetTotalAllocatedBytes(true);

    //new 方式分配數組空間
    watch.Restart();
    for (int i = 0; i < 30000; i++)
    {
        //CreateArrayDefault(i, sizes[i], false);
        var arr = new int[sizes[i]];
        for (int j = 0; j < sizes[i]; j++)
        {
            arr[j] = i;
        }
    }
    var time2 = watch.ElapsedMilliseconds;
    var gcAllocate2 = GC.GetTotalAllocatedBytes(true);

    Console.WriteLine("ArrayPool方式創建數組耗時:" + time1 + "  Gc總分配量" + (gcAllocate1 - gcAllocate0));
    Console.WriteLine("默認方式創建數組耗時:" + time2 + "  Gc總分配量" + (gcAllocate2 - gcAllocate1 - gcAllocate0));
}

內存使用截圖:左側沒有波動的橫線是緩沖池執行的過程,右側為手動創建數組的執行過程

執行結果:

ArrayPool方式創建數組耗時:17545  Gc總分配量4130800
默認方式創建數組耗時:26870  Gc總分配量37354100896

示例(前端文件通過后端Api上傳OSS)

private static void PostFileByBytesPool(FormFile file)
{
    HttpClient client = new HttpClient() { BaseAddress = new Uri("https://fileserver.com") };

    var fileLen = (int)file.Length;
    var fileArr = ArrayPool<byte>.Shared.Rent(fileLen);

    using var stream = file.OpenReadStream();
    stream.Read(fileArr, 0, fileLen);

    MultipartFormDataContent content = new MultipartFormDataContent();
    content.Add(new ByteArrayContent(fileArr, 0, fileLen), "id_" + Guid.NewGuid().ToString(), file.FileName);

    client.PostAsync("/myfile/" + file.FileName, content).Wait();
    ArrayPool<byte>.Shared.Return(fileArr, true);
}

Create()

ArrayPool 的Create()函數會創建一個 ConfigurableArrayPool 對象

ConfigurableArrayPool 的構造函數接收兩個參數

  • maxArrayLength:單次租借的數組最大長度,不可超過1024*1024*1024
  • maxArraysPerBucket:最多可以存在的未歸還緩沖區數量

通過這兩個參數可以解決Shared方式的兩個問題:

  1. 自定義單個數組的最大長度,可以獲取更大的內存空間用來存儲大文件等

  2. 限定了數組的長度和最大緩沖區數量,就限定了最大的不可回收內存數量,防止高並發時緩沖池內存持續增長

示例

//創建一個自定義緩沖池實例,單個數組最大長度為1024 * 2048,最大可同時租用10個緩沖區
ArrayPool<int> CustomerArrayPool = ArrayPool<int>.Create(1024 * 2048,10);

與Shared不同的是,如果設置CustomerArrayPool=Null那么在下一次垃圾回收時該緩沖池所占的內存會立馬全部釋放。

為防止不可預測的風險,應該保持CustomerArrayPool的存活。

同時為了防止內存的濫用應該限制CustomerArrayPool的數量

無論是Create還是Shared的方式來獲取緩沖池實例,都應該盡量保證實例所被租借的內存大小基本是差不了太多的,否則歸還的內存被復用的命中率就很低了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM