雑談NetcoreのBuffer関連の新しいタイプ

6795 ワード

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レポート
|      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を獲得する
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はシーケンスオブジェクトがutf 8のbyte[]の場合、事前に最終byte[]の長さを計算することができず、プロセス中にバッファのサイズを絶えず申請し、調整する必要がある場合があります.ArrayPoolが付加されていない場合、高周波数のシーケンス化は、高周波数でbyte[]を作成するプロセスを生成し、それに伴ってGC圧力も増大する.ArrayPoolの設計ロジックは、poolから最小長を指定するバッファを申請し、バッファは不要なときにpoolに戻り、再利用する.
var pool = ArrayPool.Shared;
var buffer = pool.Rent(1024);
//  buffer
// ...
//  
pool.Return(buffer);

Rentは申請に使用され、実際にはリース、Returnは返却され、プールに戻ります.IDisposableインタフェースを使用してReturn機能をパッケージすることができ、より便利に使用できます.
/// 
///  
/// 
/// 
public interface IArrayOwner : IDisposable
{
    /// 
    ///  
    /// 
    T[] Array { get; }

    /// 
    ///  
    /// 
    int Count { get; }
}

/// 
///  
/// 
public static class ArrayPool
{
    /// 
    ///  
    /// 
    ///  
    ///  
    /// 
    public static IArrayOwner Rent(int minLength)
    {
        return new ArrayOwner(minLength);
    }

    /// 
    ///  
    /// 
    /// 
    [DebuggerDisplay("Count = {Count}")]
    [DebuggerTypeProxy(typeof(ArrayOwnerDebugView<>))]
    private class ArrayOwner :IDisposable, IArrayOwner
    {
        /// 
        ///  
        /// 
        public T[] Array { get; }

        /// 
        ///  
        /// 
        public int Count { get; }

        /// 
        ///  
        /// 
        ///  
        public ArrayOwner(int minLength)
        {
            this.Array = ArrayPool.Shared.Rent(minLength);
            this.Count = minLength;
        }

        /// 
        ///  
        /// 
        Public void Dispose()
        {
            ArrayPool.Shared.Return(this.Array);
        }
    }

    /// 
    ///  
    /// 
    /// 
    private class ArrayOwnerDebugView
    {
        [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
        public T[] Items { get; }

        /// 
        ///  
        /// 
        /// 
        public ArrayOwnerDebugView(IArrayOwner owner)
        {
            this.Items = owner.Array.AsSpan(0, owner.Count).ToArray();
        }
    }
}

改造後の使用
using var buffer = ArrayPool.Rent(1024);
//  buffer , 

Memorypool<>


Memorypool<>は本質的にArrayPool<>を使用し、MemorypoolはRent機能のみを提供し、IMomoryOwner<>を返し、そのDisposeはReturnプロセスに等しく、使用方法は私たちが改造したArrayPool静的クラスの使用方法と同じである.

MemoryMarshal静的クラス


MemoryMarshalはツールクラスであり、ポインタ操作でよく使われるMarshalクラスと同様に、異なるベースタイプのSpanを相互変換するなど、より下位レベルのSpanまたはMemory操作を操作します.
Spanのポインタを取得
var span = new Span(new byte[] { 1, 2, 3, 4 });
ref var p0 = ref MemoryMarshal.GetReference(span);
fixed (byte* pointer = &p0)
{
    Debug.Assert(span[0] == *pointer);
}

Span汎用パラメータ型変換
Span intSpan = new Span(new int[] { 1024 });
Span byteSpan = MemoryMarshal.AsBytes(intSpan);

ReadonlyMemory<>をMemoryに変換
//  ReadonlyMemory 
Memory MemoryMarshal.AsMemory(ReadonlyMemory readonly)