dotnet高效能buffer

jiulang發表於2021-01-16

1 前言

我曾經寫過《雜談.netcore的Buffer相關新型別》的部落格,簡單介紹過BinaryPrimitives、Span<>,Memory<>,ArrayPool<>,Memorypool<>這些基礎型別,在實際專案中,我們更需要的是更上層的高效緩衝區申請、buffer寫入、buffer讀取功能。本文將介紹如何利用這些基礎型別,封裝成易於使用的buffer相關操作類,這些類的原始碼在MemoryExtensions庫裡。

2 buffer知識

buffer的申請

通過經驗與實驗資料,根據不同場景與buffer大小,選擇合適的申請方式。

申請式 特點 侷限
stackalloc byte 非常快速 堆疊上分配記憶體塊,容量小且在方法返回時緩衝區丟棄
new byte[] 當小於1KB時速度快 頻繁建立導致記憶體碎片,GC壓力大
ArrayPool.Rent 適合大的緩衝區租賃,幾乎無記憶體分配 緩衝區小於1KB時,租賃不如new來得快

IBufferWriter介面

此介面支援獲取緩衝區的寫入Span或GetMemory給外部直接寫入資料,寫入完成之後呼叫Advance(int)方法,告訴writer實際的寫入大小。

我們來對比一下MemoryStream的Write()方法,比如要寫一個int型別的值,我們不得不將int轉為4位元組的byte[],然後傳byte[]到Write()方法。這個4位元組的byte[]是一個副作用,它的存在原於外部無法獲取和擴大MemoryStream的緩衝區。

3 BufferWriter的實現

根據“buffer的申請”幾種方式,我們實現多種不同的BufferWriter。

RecyclableBufferWriter

可回收的自動擴容BufferWriter,適合於大的緩衝區的場景。它的緩衝區通過ArrayPool來租賃,用完之後,要Dispose()歸還到ArrayPool。優點是記憶體分配少,缺點是租賃比直接建立小的緩衝區還要慢。

var writer = new RecyclableBufferWriter<byte>(4);
writer.Write((byte)1);
writer.Write(new byte[] { 2, 3, 4 });
writer.WriteBigEndian(int.MaxValue);           
var writtern = writer.WrittenSpan; // 1,2,3,4,127,255,255,255

// return the buffer to pool
writer.Dispose();

ResizableBufferWriter

自動擴容的BufferWriter,適合小的動態緩衝區的場景。它的衝區通過new Array來建立,通過Array.Resize擴容。優點是cpu效能好,缺點是記憶體分配高。

var writer = new ResizableBufferWriter<byte>(4);
writer.Write((byte)1);
writer.Write(new byte[] { 2, 3, 4 });
writer.WriteBigEndian(int.MaxValue);           
var writtern = writer.WrittenSpan; // 1,2,3,4,127,255,255,255

FixedBufferWriter

固定大小緩衝區,就是我們自己new的Array,包裝為IBufferWriter物件。

var array = new byte[16];

var writer = array.CreateWriter();
writer.WriteBigEndian(18);
writer.WriteBigEndian(2.01f);

4 IBufferWriter的擴充套件

經常會遇到將int、double等諸多數字型別寫入IBufferWriter的場景,期間還涉及平臺的BigEndian或LittleEndian,我們給IBufferWriter<byte>編寫過載的擴充套件方法。

方法 說明
WriteBigEndian(this IBufferWriter, short) short
WriteBigEndian(this IBufferWriter, int) int
WriteBigEndian(this IBufferWriter, long) long
WriteBigEndian(this IBufferWriter, ushort) ushort
WriteBigEndian(this IBufferWriter, uint) uint
WriteBigEndian(this IBufferWriter, ulong) ulong
WriteBigEndian(this IBufferWriter, float) float
WriteBigEndian(this IBufferWriter, double) double
WriteLittleEndian(this IBufferWriter, short) short
WriteLittleEndian(this IBufferWriter, int) int
WriteLittleEndian(this IBufferWriter, long) long
WriteLittleEndian(this IBufferWriter, ushort) ushort
WriteLittleEndian(this IBufferWriter, uint) uint
WriteLittleEndian(this IBufferWriter, ulong) ulong
WriteLittleEndian(this IBufferWriter, float) float
WriteLittleEndian(this IBufferWriter, double) double

5 ref BufferReader

同樣的,我們也經常遇到從緩衝區中讀取為int、double等諸多數字型別的場景,所以也需要設計一個高效的BufferReader。

public ref struct BufferReader
{
    /// <summary>
    /// 未讀取的資料
    /// </summary>
    private ReadOnlySpan<byte> span;
}

給它設計ReadLittleEndian和ReadBigEndian相關Api

方法 說明
ReadBigEndian(out short) short
ReadBigEndian(out int) int
ReadBigEndian(out long) long
ReadBigEndian(out ushort) ushort
ReadBigEndian(out uint) uint
ReadBigEndian(out ulong) ulong
ReadBigEndian(out float) float
ReadBigEndian(out double) double
ReadLittleEndian(out short) short
ReadLittleEndian(out int) int
ReadLittleEndian(out long) long
ReadLittleEndian(out ushort) ushort
ReadLittleEndian(out uint) uint
ReadLittleEndian(out ulong) ulong
ReadLittleEndian(out float) float
ReadLittleEndian(out double) double

6 關於MemoryExtensions庫

本文提到的這些類或結構體,在MemoryExtensions庫裡都有實現,可以直接使用,其中BufferWriter技術已經在WebApiClient裡大量應用。

相關文章