NativeBuffering,一種高性能、零內(nèi)存分配的序列化解決方案[性能測(cè)試篇]
第一版的NativeBuffering([上篇]、[下篇])發(fā)布之后,我又對(duì)它作了多輪迭代,對(duì)性能作了較大的優(yōu)化。比如確保所有類型的數(shù)據(jù)都是內(nèi)存對(duì)齊的,內(nèi)部采用了池化機(jī)器確保真正的“零內(nèi)存分配”等。對(duì)于字典類型的數(shù)據(jù)成員,原來(lái)只是“表現(xiàn)得像個(gè)字段”,這次真正使用一段連續(xù)的內(nèi)存構(gòu)架了一個(gè)“哈希表”。我們知道對(duì)于每次.NET新版本的發(fā)布,原生的JSON序列化(System.Text.Json)的性能都作了相應(yīng)的提升,本篇文章通過(guò)具體的性能測(cè)試比較NativeBuffering和它之間的性能差異。
一、一種“特別”的序列化解決方案
二、Source Generator驅(qū)動(dòng)的編程模式
三、序列化性能比較
四、原生類型性能“友好”
五、Unmanaged 類型“性能加速”
六、無(wú)需反序列化
七、數(shù)據(jù)讀取的成本
一、一種“特別”的序列化解決方案
一般的序列化/發(fā)序列化都是數(shù)據(jù)對(duì)象和序列化結(jié)果(字符串或者單純的字節(jié)序列)之間的轉(zhuǎn)換。以下圖為例,我們定義了一個(gè)名為Person的數(shù)據(jù)類型,如果采用典型的JSON序列化方案,序列化器會(huì)將該對(duì)象轉(zhuǎn)換成一段具有JSON格式的字符串,這段字符串可以通過(guò)反序列化的方式“恢復(fù)”成一個(gè)Person對(duì)象。
如果采用NativeBuffering序列化解決方案,它會(huì)引入一個(gè)新的數(shù)據(jù)類型PersonBufferedMessage,我們采用Source Generator的方式根據(jù)Person的數(shù)據(jù)結(jié)構(gòu)自動(dòng)生成PersonBufferedMessage類型。除此之外,PersonBufferedMessage還會(huì)為Person生成額外的方式將自身對(duì)象以字節(jié)的方式寫入提供的緩沖區(qū)。
換句話說(shuō),Person對(duì)象會(huì)轉(zhuǎn)換成一段連續(xù)的字節(jié)序列化,PersonBufferedMessage就是對(duì)這段字節(jié)序列的封裝。它的數(shù)據(jù)成員(Name、Age和City)不再定義成“地段”,而被定義成“只讀屬性”,它能找到對(duì)應(yīng)的數(shù)據(jù)成員在這段字節(jié)序列中的位置,從而將其讀出來(lái)。為了提供數(shù)據(jù)讀取的性能,所有的數(shù)據(jù)成員在序列化字節(jié)序列中總是按照“原生(Native)”的形式存儲(chǔ),并且確保是內(nèi)存對(duì)齊的。也正是這個(gè)原因,NativeBuffering并不是一個(gè)跨平臺(tái)的序列化解決方案。
二、Source Generator驅(qū)動(dòng)的編程模式
NativeBuffering的整個(gè)編程圍繞著“Source Generator”進(jìn)行的,接下來(lái)我們簡(jiǎn)單演示一下如何使用它。我們?cè)谝粋€(gè)控制臺(tái)程序中添加NativeBuffering相關(guān)的兩個(gè)NuGet包NativeBuffering和NativeBuffering.Generator(使用最新版本),并定義如下這個(gè)數(shù)據(jù)類型Person。由于我們需要為Person生成額外的類型成員,我們必須將其定義成partial class。
[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" } }; }
我們?cè)陬愋蜕蠘?biāo)注BufferedMessageSourceAttribute特性將其作為BufferedMessage的“源”。此時(shí)如果我們查看VS的Solution Explorer,就會(huì)從項(xiàng)目的Depedences/Analyers/NativeBuffering.Generator看到生成的兩個(gè).cs文件。我們使用的PersonBufferedMessage就定義在PersonBufferedMessage.g.cs文件中。為Person額外添加的類型成員就定義在Person.g.cs文件中。
我們使用下的代碼來(lái)演示針對(duì)Person和PersonBufferedMessage的序列化和反序列化。如下面的代碼片段所示,我們利用Instance靜態(tài)屬性得到Person單例對(duì)象,直接調(diào)用其WriteToAsync方法(Person.g.cs文件會(huì)使Person類型實(shí)現(xiàn)IBufferedObjectSource接口,WriteToAsync方法使針對(duì)該接口定義的擴(kuò)展方法)對(duì)自身進(jìn)行序列化,并將作為序列化結(jié)果的字節(jié)序列存儲(chǔ)到指定的文件(person.bin)文件中。
using NativeBuffering; var fileName = "person.bin"; await Person.Instance.WriteToAsync(fileName); using (var pooledBufferedMessage = await BufferedMessage.LoadAsync<PersonBufferedMessage>(fileName)) { var bufferedMessage = pooledBufferedMessage.BufferedMessage; Console.WriteLine( @$"{nameof(bufferedMessage.Name),-17}: {bufferedMessage.Name} {nameof(bufferedMessage.Age),-17}: {bufferedMessage.Age} {nameof(bufferedMessage.Hobbies),-17}: {string.Join(", ", bufferedMessage.Hobbies)} {nameof(bufferedMessage.Address),-17}: {bufferedMessage.Address} {nameof(bufferedMessage.PhoneNumber),-17}: {bufferedMessage.PhoneNumber} {nameof(bufferedMessage.Email),-17}:{bufferedMessage.Email} {nameof(bufferedMessage.Nationality),-17}:{bufferedMessage.Nationality}, {nameof(bufferedMessage.Occupation),-17}:{bufferedMessage.Occupation}, {nameof(bufferedMessage.EducationLevel),-17}:{bufferedMessage.EducationLevel} {nameof(bufferedMessage.MaritalStatus),-17}:{bufferedMessage.MaritalStatus} {nameof(bufferedMessage.SpouseName),-17}:{bufferedMessage.SpouseName} {nameof(bufferedMessage.NumberOfChildren),-17}:{bufferedMessage.NumberOfChildren} {nameof(bufferedMessage.ChildrenNames),-17}: {string.Join(", ", bufferedMessage.ChildrenNames)} {nameof(bufferedMessage.LanguagesSpoken),-17}: {string.Join(", ", bufferedMessage.LanguagesSpoken)} {nameof(bufferedMessage.HasPets),-17}:{bufferedMessage.HasPets} {nameof(bufferedMessage.PetNames),-17}: {string.Join(", ", bufferedMessage.PetNames)}"); }
然后我們調(diào)用BufferedMessage的靜態(tài)方法LoadAsync<PersonBufferedMessage>加載該文件的內(nèi)容。該方法會(huì)返回一個(gè)PooledBufferedMessage<PersonBufferedMessage>對(duì)象,它的BufferedMessage返回我們需要的PersonBufferedMessage對(duì)象。PersonBufferedMessage具有與Person一致的數(shù)據(jù)成員,我們將它們的內(nèi)容一一輸出,可以看出PersonBufferedMessage承載的內(nèi)容與Person對(duì)象使完全一致的。
NativeBuffering之所以能供實(shí)現(xiàn)真正意義的“零內(nèi)存分配”,得益于對(duì)“池化機(jī)制”的應(yīng)用。LoadAsync<T>方法返回的PooledBufferedMessage<T>使用一段池化的緩存區(qū)來(lái)存儲(chǔ)序列化的字節(jié),當(dāng)我們不再使用的時(shí)候,需要調(diào)用其Dispose方法緩存區(qū)釋放到緩存池內(nèi)。
三、序列化性能比較
接下來(lái)我們以就以上面定義的Person類型為例,利用BenchmarkDotNet比較一下NativeBuffering與JSON序列化在性能上的差異。如下面的代碼片段所示,針對(duì)JSON序列化的Benchmark方法直接調(diào)用JsonSerializer的Serialize方法將Person單例對(duì)象序列化成字符串。
[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!); } }
在針對(duì)NativeBuffering的Benchmark方法中,我們調(diào)用Person單例對(duì)象的WriteTo擴(kuò)展方法對(duì)它進(jìn)行序列化,并利用一個(gè)ArraySegment<T>結(jié)構(gòu)返回序列化結(jié)果。WriteTo方法具有一個(gè)類型為Func<int, byte[]>的參數(shù),我們使用它來(lái)提供一個(gè)存放序列化結(jié)果的字節(jié)數(shù)組。作為Func<int, byte[]>輸入?yún)?shù)的整數(shù)代表序列化結(jié)果的字節(jié)長(zhǎng)度,這樣我們才能確保提供的字節(jié)數(shù)組具有充足的存儲(chǔ)空間。
為了避免內(nèi)存分配,我們利用這個(gè)委托從ArrayPool<byte>.Shared表示的“數(shù)組池”中借出一個(gè)大小適合的字節(jié)數(shù)組,并在完成序列化之后將其釋放。這段性能測(cè)試結(jié)果如下,可以看出從耗時(shí)來(lái)看,針對(duì)NativeBuffering的序列化稍微多了一點(diǎn),但是從內(nèi)存分配來(lái)看,它真正做到了內(nèi)存的“零分配”,而JSON序列化則分配了1K多的內(nèi)存。
四、原生類型性能“友好”
從上面展示的性能測(cè)試結(jié)果可以看出,NativeBuffering在序列化上確實(shí)可以不用分配額外的內(nèi)存,但是耗時(shí)似乎多了點(diǎn)。那么是否意味著NativeBuffering不如JSON序列化高效嗎?其實(shí)也不能這么說(shuō)。NativeBuffering會(huì)使用一段連續(xù)的內(nèi)存(而不是多段緩存的拼接)來(lái)存儲(chǔ)序列化結(jié)果,所以它在序列化之前需要先計(jì)算字節(jié)數(shù)。由于Person定義的絕大部分?jǐn)?shù)據(jù)成員都是字符串,這導(dǎo)致了它需要計(jì)算字符串編碼后的字節(jié)數(shù),這個(gè)計(jì)算會(huì)造成一定的耗時(shí)。
所以字符串不是NativeBuffering的強(qiáng)項(xiàng),對(duì)于其他數(shù)據(jù)類型,NativeBuffering性能其實(shí)很高的?,F(xiàn)在我們重新定義如下這個(gè)名為Entity的數(shù)據(jù)類型,它將常用的Primitive類型和一個(gè)字節(jié)數(shù)組作為數(shù)據(jù)成員
[BufferedMessageSource] public partial class Entity { public byte ByteValue { get; set; } public sbyte SByteValue { get; set; } public short ShortValue { get; set; } public ushort UShortValue { get; set; } public int IntValue { get; set; } public uint UIntValue { get; set; } public long LongValue { get; set; } public ulong ULongValue { get; set; } public float FloatValue { get; set; } public double DoubleValue { get; set; } public decimal DecimalValue { get; set; } public bool BoolValue { get; set; } public char CharValue { get; set; } public byte[] Bytes { get; set; } public static Entity Instance = new Entity { ByteValue = 1, SByteValue = 2, ShortValue = 3, UShortValue = 4, IntValue = 5, UIntValue = 6, LongValue = 7, ULongValue = 8, FloatValue = 9, DoubleValue = 10, DecimalValue = 11, BoolValue = true, CharValue = 'a', Bytes = Enumerable.Range(0, 128).Select(it => (byte)it).ToArray() }; }
然后我們將性能測(cè)試的兩個(gè)Benchmark方法使用的數(shù)據(jù)類型從Person改為Entity。
[MemoryDiagnoser] public class Benchmark { private static readonly Func<int, byte[]> _bufferFactory = ArrayPool<byte>.Shared.Rent; [Benchmark] public string SerializeAsJson() => JsonSerializer.Serialize(Entity.Instance); [Benchmark] public void SerializeNativeBuffering() { var arraySegment = Entity.Instance.WriteTo(_bufferFactory); ArrayPool<byte>.Shared.Return(arraySegment.Array!); } }
再來(lái)看看如下的測(cè)試結(jié)果,可以看出NativeBuffering序列化的耗時(shí)差不多是JSON序列化的一半,并且它依然沒(méi)有任何內(nèi)存分配。
五、Unmanaged 類型“性能加速”
NativeBuffering不僅僅對(duì)Primitive類型“友好”,對(duì)于自定義的Unmanaged結(jié)構(gòu),更能體現(xiàn)其性能優(yōu)勢(shì)。原因很簡(jiǎn)單,Unmanaged類型(含Primitive類型和自定義的unmanaged結(jié)構(gòu))的內(nèi)存布局就是連續(xù)的,NativeBuffering在進(jìn)行序列化的適合不需要對(duì)它進(jìn)行“分解”,直接拷貝這段內(nèi)存的內(nèi)容就可以了。
作為演示,我們定義了如下這個(gè)Foobarbazqux結(jié)構(gòu)體,可以看出它滿足unmanaged結(jié)構(gòu)的要求。作為序列化數(shù)據(jù)類型的Record中,我們定義了一個(gè)Foobarbazqux數(shù)組類型的屬性Data。Instance靜態(tài)字段表示的單例對(duì)象的Data屬性包含100個(gè)Foobarbazqux對(duì)象。
[BufferedMessageSource] public partial class Record { public Foobarbazqux[] Data { get; set; } = default!; public static Record Instance = new Record { Data = Enumerable.Range(1, 100).Select(_ => new Foobarbazqux(new Foobarbaz(new Foobar(111, 222), 1.234f), 3.14d)).ToArray()}; } public readonly record struct Foobar(int Foo, long Bar); public readonly record struct Foobarbaz(Foobar Foobar, float Baz); public readonly record struct Foobarbazqux(Foobarbaz Foobarbaz, double Qux);
我們同樣只需要將性能測(cè)試的數(shù)據(jù)類型改成上面定義的Record就可以了。
[MemoryDiagnoser] public class Benchmark { private static readonly Func<int, byte[]> _bufferFactory = ArrayPool<byte>.Shared.Rent; [Benchmark] public string SerializeAsJson() => JsonSerializer.Serialize(Record.Instance); [Benchmark] public void SerializeNativeBuffering() { var arraySegment = Record.Instance.WriteTo(_bufferFactory); ArrayPool<byte>.Shared.Return(arraySegment.Array!); } }
這次NativeBuffering針對(duì)JSON序列化的性能優(yōu)勢(shì)完全是“碾壓式”的。耗時(shí):72us/3us。JSON序列化不僅帶來(lái)了26K的內(nèi)存分配,還將部分內(nèi)存提升到了Gen1。
六、無(wú)需反序列化
對(duì)于序列化來(lái)說(shuō),NativeBuffering不僅僅可以避免內(nèi)存的分配。如果不是大規(guī)模涉及字符串,它在耗時(shí)方面依然具有很大的優(yōu)勢(shì)。即使大規(guī)模使用字符串,考慮到JSON字符串最終還是需要編碼轉(zhuǎn)換成字節(jié)序列化,兩者之間的總體耗時(shí)其實(shí)差別也不大。NativeBuffering針對(duì)反序列化的性能優(yōu)勢(shì)更是毋庸置疑,因?yàn)槲覀兪褂玫腂ufferedMessage就是對(duì)序列化結(jié)果的封裝,所以反序列化的成本幾乎可以忽略(經(jīng)過(guò)測(cè)試耗時(shí)在幾納秒)。
為了讓大家能夠感覺(jué)到與JSON分序列化的差異,我們將讀取數(shù)據(jù)成員的操作也作為反序列化的一部分。如下面這個(gè)Benchmark所示,我們?cè)诔跏蓟詣?dòng)執(zhí)行的Setup方法中,針對(duì)同一個(gè)Entity對(duì)象的兩種序列化結(jié)果(字節(jié)數(shù)組)存儲(chǔ)在_encodedJson 和_payload字段中。
[MemoryDiagnoser] public class Benchmark { private byte[] _encodedJson = default!; private byte[] _payload = default!; [GlobalSetup] public void Setup() { _encodedJson = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(Entity.Instance)); _payload = new byte[Entity.Instance.CalculateSize()]; Person.Instance.WriteToAsync(new MemoryStream(_payload), true); } [Benchmark] public void DeserializeFromJson() { var entity = JsonSerializer.Deserialize<Entity>(Encoding.UTF8.GetString(_encodedJson))!; Process(entity.ByteValue); Process(entity.SByteValue); Process(entity.ShortValue); Process(entity.UShortValue); Process(entity.IntValue); Process(entity.UIntValue); Process(entity.LongValue); Process(entity.ULongValue); Process(entity.FloatValue); Process(entity.DoubleValue); Process(entity.DecimalValue); Process(entity.BoolValue); Process(entity.CharValue); Process(entity.Bytes); } [Benchmark] public void DeserializeFromNativeBuffering() { unsafe { fixed (byte* _ = _payload) { var entity = new EntityBufferedMessage(new NativeBuffer(_payload)); Process(entity.ByteValue); Process(entity.SByteValue); Process(entity.ShortValue); Process(entity.UShortValue); Process(entity.IntValue); Process(entity.UIntValue); Process(entity.LongValue); Process(entity.ULongValue); Process(entity.FloatValue); Process(entity.DoubleValue); Process(entity.DecimalValue); Process(entity.BoolValue); Process(entity.CharValue); Process(entity.Bytes); } } } [MethodImpl(MethodImplOptions.NoInlining)] private void Process<T>(T expected) { } }
針對(duì)JSON反序列化的Benchmark方法利用JsonSerializer將解碼生成的字符串反序列化成Entity對(duì)象,并調(diào)用Process方法讀取每個(gè)數(shù)據(jù)成員。在針對(duì)NativeBuffering的Benchmark方法中,我們需要?jiǎng)?chuàng)建一個(gè)fixed上下文將字節(jié)數(shù)組內(nèi)存地址固定,因?yàn)锽ufferedMessage的讀取涉及很多Unsafe的內(nèi)存地址操作,然后將這個(gè)字節(jié)數(shù)組封裝成NativeBuffer對(duì)象,并據(jù)此將EntityBufferedMessage創(chuàng)建出來(lái)。這個(gè)方法的耗時(shí)花在后面針對(duì)數(shù)據(jù)成員的讀取上。如下所示的兩種“反序列”方式的測(cè)試結(jié)果。從如下所示的測(cè)試結(jié)果可以看出相對(duì)于NativeBuffering的無(wú)需反序列化,JSON反序列化的成本還是巨大的,不僅反映在耗時(shí)上,同時(shí)也反映在內(nèi)存分配上。
七、數(shù)據(jù)讀取的成本
上面的測(cè)試結(jié)果也體現(xiàn)了NativeBuffering針對(duì)數(shù)據(jù)讀取的成本。和普通類型直接讀取字段的值不同,NativeBuffering生成的BufferedMessage對(duì)象是對(duì)一段連續(xù)字節(jié)序列的封裝,此字節(jié)序列就是序列化的結(jié)果。如下所示的是這段字節(jié)序列的布局:整個(gè)序列包括兩個(gè)部分,后面一部分依次存儲(chǔ)每個(gè)字段的內(nèi)容,前面一部分則存儲(chǔ)每個(gè)字段內(nèi)容在整個(gè)字節(jié)序列的位置(偏移量)。
BufferedMessage的每個(gè)數(shù)據(jù)成員都是只讀屬性,針對(duì)數(shù)據(jù)成員的讀取至少需要兩個(gè)步驟:
- 根據(jù)數(shù)據(jù)成員的序號(hào)讀取存儲(chǔ)內(nèi)容的偏移量;
- 將偏移量轉(zhuǎn)換成內(nèi)存地址,結(jié)合當(dāng)前數(shù)據(jù)類型將數(shù)據(jù)讀出來(lái);
所以NativeBuffering最大的問(wèn)題就是:讀取數(shù)據(jù)成員的性能肯定比直接讀取字段值要高。從上面的測(cè)試結(jié)果大體可以測(cè)出單次讀取耗時(shí)大體在1-2納米之間(24.87ns包括創(chuàng)建EntityBufferedMessage和調(diào)用空方法Process的耗時(shí)),也就是說(shuō)1秒中可以完成5-10億次讀取。我想這個(gè)讀取成本大部分應(yīng)用是可以接受的,尤其是相對(duì)于它在序列化/反序列化在耗時(shí)和內(nèi)存分配帶來(lái)的巨大優(yōu)勢(shì)來(lái)說(shuō),讀取數(shù)據(jù)成員帶來(lái)時(shí)間損耗基本上可以忽略了。
NativeBuffering針對(duì)字符串序列化的性能在最新版本中已經(jīng)得到提升,并完全超越了System.Text.Json序列化的性能,具體的性能測(cè)試結(jié)果和背后的原理可以參閱《性能測(cè)試?yán)m(xù)篇》。


第一版的NativeBuffering([上篇]、[下篇])發(fā)布之后,我又對(duì)它作了多輪迭代,對(duì)性能作了較大的優(yōu)化。比如確保所有類型的數(shù)據(jù)都是內(nèi)存對(duì)齊的,內(nèi)部采用了池化機(jī)器確保真正的“零內(nèi)存分配”等。對(duì)于字典類型的數(shù)據(jù)成員,原來(lái)只是“表現(xiàn)得像個(gè)字段”,這次真正使用一段連續(xù)的內(nèi)存構(gòu)架了一個(gè)“哈希表”。我們知道對(duì)于每次.NET新版本的發(fā)布,原生的JSON序列化(System.Text.Json)的性能都作了相應(yīng)的提升,本篇文章通過(guò)具體的性能測(cè)試比較NativeBuffering和它之間的性能差異。








浙公網(wǎng)安備 33010602011771號(hào)