• NativeBuffering,一种高性能、零内存分配的序列化解决方案[性能测试续篇]


    在《NativeBuffering,一种高性能、零内存分配的序列化解决方案[性能测试篇]》我比较了NativeBuffering和System.Text.Json两种序列化方式的性能,通过性能测试结果可以看出NativeBuffering具有非常明显的优势,有的方面的性能优势甚至是“碾压式”的,唯独针对字符串的序列化性能不够理想。我趁这个周末对此做了优化,解决了这块短板,接下来我们就来看看最新的性能测试结果和背后“加速”的原理。[NativeBuffering@github]

    一、新版的性能测试结果

    我使用《NativeBuffering,一种高性能、零内存分配的序列化解决方案[性能测试篇]》提供的测试用例,选用的依然是如下这个Person类型,它的绝大部分数据成员都是字符串。

    复制代码
    [BufferedMessageSource]
    public partial class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public string[] Hobbies { get; set; }
        public string Address { get; set; }
        public string PhoneNumber { get; set; }
        public string Email { get; set; }
        public string Gender { get; set; }
        public string Nationality { get; set; }
        public string Occupation { get; set; }
        public string EducationLevel { get; set; }
        public string MaritalStatus { get; set; }
        public string SpouseName { get; set; }
        public int NumberOfChildren { get; set; }
        public string[] ChildrenNames { get; set; }
        public string[] LanguagesSpoken { get; set; }
        public bool HasPets { get; set; }
        public string[] PetNames { get; set; }
    
        public static Person Instance = new Person
        {
            Name = "Bill",
            Age = 30,
            Hobbies = new string[] { "Reading", "Writing", "Coding" },
            Address = "123 Main St.",
            PhoneNumber = "555-555-5555",
            Email = "bill@gmail.com",
            Gender = "M",
            Nationality = "China",
            Occupation = "Software Engineer",
            EducationLevel = "Bachelor's",
            MaritalStatus = "Married",
            SpouseName = "Jane",
            NumberOfChildren = 2,
            ChildrenNames = new string[] { "John", "Jill" },
            LanguagesSpoken = new string[] { "English", "Chinese" },
            HasPets = true,
            PetNames = new string[] { "Fido", "Spot" }
        };
    }
    复制代码

    这是采用的测试案例。Benchmark方法SerializeAsJson直接将静态字段Instance表示的Person对象序列化成JSON字符串,采用NativeBuffering的Benchmark方法SerializeAsNativeBuffering直接调用WriteTo扩展方法(通过Source Generator生成)对齐进行序列化,并利用一个ArraySegment结构返回序列化结果。WriteTo方法具有一个类型为Func<int, byte[]>的参数,我们使用它来提供一个存放序列化结果的字节数组。作为Func<int, byte[]>输入参数的整数代表序列化结果的字节长度,这样我们才能确保提供的字节数组具有充足的存储空间。

    复制代码
    [MemoryDiagnoser]
    public class Benchmark
    {
        private  static readonly Func<int, byte[]> _bufferFactory = ArrayPool<byte>.Shared.Rent;
    
        [Benchmark]
        public string SerializeAsJson() => JsonSerializer.Serialize(Person.Instance);
    
        [Benchmark]
        public void SerializeNativeBuffering()
        {
            var arraySegment = Person.Instance.WriteTo(_bufferFactory);
            ArrayPool<byte>.Shared.Return(arraySegment.Array!);
        }
    }
    复制代码

    这是上一个版本的测试结果,虽然NativeBuffering具有“零内存分配”的巨大优势,但是在耗时上会多一些。造成这个劣势的主要原因来源于针对字符串的编码,因为NativeBuffering在序列化过程需要涉及两次编码,一次是为了计算总的字节数,另一次才是生成序列化结果。

    image

    如果切换到目前最新版本(0.1.5),可以看出NativeBuffering的性能已经得到了极大的改善,并且明显优于JSON序列化的性能(对于JSON序列化,两次测试具体的耗时之所以具有加大的差异,是因为测试机器配置不同,12代和13代i7的差异)。而在内存分配层面,针对NativeBuffering的序列化依然是“零分配”。

    image

    二、背后的故事

    接下来我们就来简单说明一下为什么NativeBuffering针对字符串的序列化明显优于JSON序列化,这要从BufferedString这个自定义的结构说起。如下所示的就是Source Generator为Person类型生成的BufferedMessage类型,可以看出它的原有的字符串类型的成员在此类型中全部转换成了BufferedString类型的只读属性。

    复制代码
    public unsafe readonly struct PersonBufferedMessage : IReadOnlyBufferedObject
    {
        public static PersonBufferedMessage DefaultValue => throw new NotImplementedException();
        public NativeBuffer Buffer { get; }
        public PersonBufferedMessage(NativeBuffer buffer) => Buffer = buffer;
        public static PersonBufferedMessage Parse(NativeBuffer buffer) => new PersonBufferedMessage(buffer);
        public BufferedString Name => Buffer.ReadNonNullableBufferedObjectField(0);
        public System.Int32 Age => Buffer.ReadUnmanagedField(1);
        public ReadOnlyNonNullableBufferedObjectList Hobbies => Buffer.ReadNonNullableBufferedObjectCollectionField(2);
        public BufferedString Address => Buffer.ReadNonNullableBufferedObjectField(3);
        public BufferedString PhoneNumber => Buffer.ReadNonNullableBufferedObjectField(4);
        public BufferedString Email => Buffer.ReadNonNullableBufferedObjectField(5);
        public BufferedString Gender => Buffer.ReadNonNullableBufferedObjectField(6);
        public BufferedString Nationality => Buffer.ReadNonNullableBufferedObjectField(7);
        public BufferedString Occupation => Buffer.ReadNonNullableBufferedObjectField(8);
        public BufferedString EducationLevel => Buffer.ReadNonNullableBufferedObjectField(9);
        public BufferedString MaritalStatus => Buffer.ReadNonNullableBufferedObjectField(10);
        public BufferedString SpouseName => Buffer.ReadNonNullableBufferedObjectField(11);
        public System.Int32 NumberOfChildren => Buffer.ReadUnmanagedField(12);
        public ReadOnlyNonNullableBufferedObjectList ChildrenNames => Buffer.ReadNonNullableBufferedObjectCollectionField(13);
        public ReadOnlyNonNullableBufferedObjectList LanguagesSpoken => Buffer.ReadNonNullableBufferedObjectCollectionField(14);
        public System.Boolean HasPets => Buffer.ReadUnmanagedField(15);
        public ReadOnlyNonNullableBufferedObjectList PetNames => Buffer.ReadNonNullableBufferedObjectCollectionField(16);
    }
    复制代码

    BufferedString在NativeBuffering中用来表示字符串。如代码片段所示,BufferedString 同样实现了IReadOnlyBufferedObject接口,以为着它也是对一段字节序列的封装。BufferedString提供了针对字符串类型的隐式转换,所以我们在编程的时候可以将它当成普通字符串来使用。

    复制代码
    public unsafe readonly struct BufferedString : IReadOnlyBufferedObject
    {
        public static BufferedString DefaultValue { get; }
        static BufferedString()
        {
            var size = CalculateStringSize(string.Empty);
            var bytes = new byte[size];
    
            var context = BufferedObjectWriteContext.Create(bytes);
            context.WriteString(string.Empty);
            DefaultValue = new BufferedString(new NativeBuffer(bytes));
        }
        public BufferedString(NativeBuffer buffer) => _start = buffer.Start;
        public BufferedString(void* start) => _start = start;
    
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static BufferedString Parse(NativeBuffer buffer) =>  new(buffer);
    
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static BufferedString Parse(void* start) => new(start);
    
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int CalculateSize(void* start) => Unsafe.Read<int>(start);
    
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public string AsString()
        {
            string v = default!;
            Unsafe.Write(Unsafe.AsPointer(ref v), new IntPtr(Unsafe.Add<byte>(_start, IntPtr.Size * 2)));
            return v;
        }
    
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static implicit operator string(BufferedString value) => value.AsString();
    
        public override string ToString() => AsString();
    
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int CalculateStringSize(string? value)
        {
            var byteCount = value is null ? 0 : Encoding.Unicode.GetByteCount(value);
            var size = _headerByteCount + byteCount;
            return Math.Max(IntPtr.Size * 3 + sizeof(int), size);
        }
    
        private static readonly int _headerByteCount = sizeof(nint) + sizeof(nint) + sizeof(nint) + sizeof(int);
    }
    复制代码

    值得一提的是,BufferedString向String的类型转换是没有任何开销的,这一切源自它封装的这段字节序列的结构。我曾经在《你知道.NET的字符串在内存中是如何存储的吗?》中介绍过字符串对象自身在内存中的布局,而BufferedString封装的字节序列就是在这段内容加上前置的4/8个字节(x84为4字节,x64需要添加4字节Padding确保内存对齐)来表示总的字节数。当BufferedString转换成String类型时,只需要将返回的字符串变量指向TypeHandle部分的地址就可以了,这一点体现在上述的AsString方法上。

    image

    也正是因为NativeBuffering在序列化字符串的时候,生成的字节序列与字符串对象的内存布局一致,所以不在需要对字符串进行编码,直接按照如下所示的方式进行内存拷贝就可以了。这正是NativeBuffering针对字符串的序列化的性能得以提升的原因,不过整个序列化过程中还是需要计算字符串针对默认编码(Unicode)的字节长度。

    image

  • 相关阅读:
    html动态新增div元素
    Redisi消息队列
    超声波模块详细介绍(stm32循迹小车中超声波的介绍)
    计算机网络408 2017
    Seurat 中的数据可视化方法
    CSS:层叠规则
    20多年前微软曾计划收购,任天堂嘲讽道:“笑死我了”
    CoinGecko 播客:与 Cartesi 联合创始人 Erick 一起构建 Layer-2
    详解设计模式:抽象工厂模式
    SpringCloud学习笔记
  • 原文地址:https://www.cnblogs.com/artech/p/native-buffering-buffered-string.html