雜談.netcore的Buffer相關新類型

1 文章範圍

本文將.netcore新出現的與Buffer操做相關的類型進行簡單分析與講解,因爲資料有限,一些看法爲我的看法,可能不是很準確。這些新類型將包括BinaryPrimitives、Span<>,Memory<>,ArrayPool<>,Memorypool<>數組

2 BinaryPrimitives

在網絡傳輸中,最小單位是byte,不少場景,咱們須要將int long short等類型與byte[]相互轉換。好比,將int轉換爲BigEndian的4個字節,在過去,咱們很容易就想到BitConverter,但BitConverter設計得不夠好友,BitConverter.GetBytes(int value)獲得的byte[]的字節順序永遠與主機的字節順序同樣,咱們不得再也不根據BitConverter的IsLittleEndian屬性判斷是否須要對獲得byte[]進行轉換字節順序,而BinaryPrimitives的Api設計爲嚴格區分Endian,每一個Api都指定了目標Endian。網絡

BitConverter異步

var intValue = 1;
var bytes = BitConverter.GetBytes(intValue);
if (BitConverter.IsLittleEndian == true)
{
    Array.Reverse(bytes);
}

BinaryPrimitives工具

var intValue = 1;
var bytes = new byte[sizeof(int)];
BinaryPrimitives.WriteInt32BigEndian(bytes, intValue);

3 Span<>

Span 是一個高效的連續內存範圍操做值類型,咱們知道Array 是一個鏈接的內存範圍的引用類型,那爲何還須要Span類型呢?能夠簡單這麼認爲:Span除了提供更高性能的Array的讀寫功能以外,還提供了比ArraySegment更易於理解和使用的內存局部視圖,也就是說Span功能包含了Array+ArraySegment的功能,我可使用BenchmarkDotNet對比Span、Array和指針讀寫一個鏈接內存的性能比較,測試結果爲Span>Pointer>Array: 性能

讀寫代碼測試

public class DemoContext
{
    private byte[] array = new byte[1024];

    [Benchmark]
    public void ByteArray()
    {            
        for (var i = 0; i < array.Length; i++)
        {
            array[i] = array[i];
        }
    }

    [Benchmark]
    public void ByteSpan()
    {
        var span = array.AsSpan();
        for (var i = 0; i < span.Length; i++)
        {
            span[i] = span[i];
        }
    }

    [Benchmark]
    unsafe public void BytePointer()
    {
        fixed (byte* pointer = &array[0])
        {
            for (var i = 0; i < array.Length; i++)
            {
                *(pointer + i) = *(pointer + i);
            }
        }
    }
}

Benchmark報告this

|      Method |     Mean |   Error |  StdDev |
|------------ |---------:|--------:|--------:|
|   ByteArray | 577.4 ns | 9.07 ns | 8.48 ns |
|    ByteSpan | 323.8 ns | 0.87 ns | 0.81 ns |
| BytePointer | 499.4 ns | 4.09 ns | 3.82 ns |

Memory<>

若是嘗試將Span<>做爲全局變量,或在異步方法聲明爲變量,你會獲得編譯器的錯誤,緣由不在本文講解範圍內,而Memory<>類型能夠知足這些需求,Memory<>提供了用於數據讀寫的Span屬性,這個Span屬性是每將獲取時都有一些計算,因此咱們應該儘可能避免屢次獲取它的Span屬性。spa

合理的獲取Span.net

var span = memory.Span;
for (var i = 0; i < span.Length; i++)
{
    span[i] = span[i];
}

不合理的獲取Span設計

for (var i = 0; i < memory.Length; i++)
{
    memory.Span[i] = memory.Span[i];
}

Benchmark報告

|      Method |       Mean |    Error |   StdDev |
|------------ |-----------:|---------:|---------:|
| ByteMemory1 |   325.8 ns |  1.03 ns |  0.97 ns |
| ByteMemory2 | 3,344.9 ns | 11.91 ns | 11.14 ns |

ArrayPool<>

ArrayPool<>用於解決頻繁申請內存和釋放內存致使GC壓力過大的場景,好比System.Text.Json在序列對象時爲utf8的byte[]時,事先是沒法計算最終byte[]的長度的,過程當中可能要不斷申請和調整緩衝區的大小。在沒有ArrayPool加持的狀況下,高頻次的序列化,則會生產高頻建立byte[]的過程,隨之GC壓力也會增大。ArrayPool的設計邏輯是,從pool申請一個指定最小長度的緩衝區,緩衝區在不須要的時候,將其返回到pool裏,待以重複利用。

var pool = ArrayPool<byte>.Shared;
var buffer = pool.Rent(1024);
// 開始利用buffer
// ...
// 使用結束
pool.Return(buffer);

Rent用於申請,其實是租賃,Return是歸還,返回到池中。咱們可使用IDisposable接口來包裝Return功能,使用上更方便一些:

/// <summary>
/// 定義數組持有者的接口
/// </summary>
/// <typeparam name="T"></typeparam>
public interface IArrayOwner<T> : IDisposable
{
    /// <summary>
    /// 獲取持有的數組
    /// </summary>
    T[] Array { get; }

    /// <summary>
    /// 獲取數組的有效長度
    /// </summary>
    int Count { get; }
}

/// <summary>
/// 表示共享的數組池
/// </summary>
public static class ArrayPool
{
    /// <summary>
    /// 租賃數組
    /// </summary>
    /// <typeparam name="T">元素類型</typeparam>
    /// <param name="minLength">最小長度</param>
    /// <returns></returns>
    public static IArrayOwner<T> Rent<T>(int minLength)
    {
        return new ArrayOwner<T>(minLength);
    }

    /// <summary>
    /// 表示數組持有者
    /// </summary>
    /// <typeparam name="T"></typeparam>
    [DebuggerDisplay("Count = {Count}")]
    [DebuggerTypeProxy(typeof(ArrayOwnerDebugView<>))]
    private class ArrayOwner<T> :IDisposable, IArrayOwner<T>
    {
        /// <summary>
        /// 獲取持有的數組
        /// </summary>
        public T[] Array { get; }

        /// <summary>
        /// 獲取數組的有效長度
        /// </summary>
        public int Count { get; }

        /// <summary>
        /// 數組持有者
        /// </summary>
        /// <param name="minLength"></param> 
        public ArrayOwner(int minLength)
        {
            this.Array = ArrayPool<T>.Shared.Rent(minLength);
            this.Count = minLength;
        }

        /// <summary>
        /// 歸還數組
        /// </summary>
        Public void Dispose()
        {
            ArrayPool<T>.Shared.Return(this.Array);
        }
    }

    /// <summary>
    /// 調試視圖
    /// </summary>
    /// <typeparam name="T"></typeparam>
    private class ArrayOwnerDebugView<T>
    {
        [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
        public T[] Items { get; }

        /// <summary>
        /// 調試視圖
        /// </summary>
        /// <param name="owner"></param>
        public ArrayOwnerDebugView(IArrayOwner<T> owner)
        {
            this.Items = owner.Array.AsSpan(0, owner.Count).ToArray();
        }
    }
}

改造以後的使用

using var buffer = ArrayPool.Rent<byte>(1024);
// 盡情的使用buffer吧,自動回收

Memorypool<>

Memorypool<>本質上仍是使用了ArrayPool<>,Memorypool只提供了Rent功能,返回一個IMomoryOwner<>,對其Dispose等同於Return過程,使用方式和咱們上面改造過的ArrayPool靜態類的使用方式是同樣的。

MemoryMarshal靜態類

MemoryMarshal是一個工具類,相似於咱們指針操做時經常用到的Marshal類,它操做一些更底層的Span或Memory操做,好比提供將不一樣基元類型的Span相互轉換等。

獲取Span的指針

var span = new Span<byte>(new byte[] { 1, 2, 3, 4 });
ref var p0 = ref MemoryMarshal.GetReference(span);
fixed (byte* pointer = &p0)
{
    Debug.Assert(span[0] == *pointer);
}

Span泛型參數類型轉換

Span<int> intSpan = new Span<int>(new int[] { 1024 });
Span<byte> byteSpan = MemoryMarshal.AsBytes(intSpan);

ReadonlyMemory<>轉換爲Memory

// 至關於給ReadonlyMemory移除只讀功能
Memory<T> MemoryMarshal.AsMemory<T>(ReadonlyMemory<T> readonly)
相關文章
相關標籤/搜索