• 杂谈.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报告

    |      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属性。

    合理的获取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在序列对象时为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)
    
  • 相关阅读:
    数据库三级考试的随笔2.0
    数据库三级考试的随笔1.2
    数据库三级考试的随笔1.1
    数据库三级考试的随笔1.0
    陈贤文的第一次试水
    点分治总结
    Treap总结
    UML之二、建模元素(1)
    StarUML之九、starUML的一些特殊属性的说明
    UML之一、为什么需要UML?
  • 原文地址:https://www.cnblogs.com/kewei/p/12183258.html
Copyright © 2020-2023  润新知