编辑:[db:作者] 时间:2024-08-25 01:45:40
由于"大众年夜众号排版缘故原由,译者建议大家在桌面端阅读本文,手机阅读体验并不是很好。
简介我发布了一个名为MemoryPack[1] 的新序列化程序,这是一种特定于 C# 的新序列化程序,其实行速率比其他序列化程序快得多。
与MessagePack for C#[2] (一个快速的二进制序列化程序)比较标准工具的序列化库性能快几倍,当数据最优时,性能乃至快 50~100 倍。最好的支持是.NET 7,但现在支持.NET Standard 2.1(.NET 5,6),Unity 乃至 TypeScript。它还支持多态性(Union),完全版本容错,循环引用和最新的当代 I/O API(IBufferWriter,ReadOnlySeqeunce,Pipelines)。
序列化程序的性能基于“数据格式规范”和“每种措辞的实现”。例如,虽然二进制格式常日比文本格式(如 JSON)具有上风,但 JSON 序列化程序可能比二进制序列化程序更快(如Utf8Json[3] 所示)。那么最快的序列化程序是什么?当你同时理解规范和实现时,真正最快的序列化程序就出身了。
多年来,我一贯在开拓和掩护 MessagePack for C#,而 MessagePack for C# 是 .NET 天下中非常成功的序列化程序,拥有超过 4000 颗 GitHub 星。它也已被微软标准产品采取,如 Visual Studio 2022,SignalR MessagePack Hub[4]协议和 Blazor Server 协议(blazorpack)。
在过去的 5 年里,我还处理了近 1000 个问题。自 5 年前以来,我一贯在利用 Roslyn 的代码天生器进行 AOT 支持,并对其进行了演示,尤其是在 Unity、AOT 环境 (IL2CPP) 以及许多利用它的 Unity 手机游戏中。
除了 MessagePack for C# 之外,我还创建了ZeroFormatter[5](自己的格式)和Utf8Json[6](JSON)等序列化程序,它们得到了许多 GitHub Star,以是我对不同格式的性能特色有深刻的理解。此外,我还参与了 RPC 框架MagicOnion[7],内存数据库MasterMemory[8],PubSub 客户端AlterNats[9]以及几个游戏的客户端(Unity)/做事器实现的创建。
MemoryPack 的目标是成为终极的快速,实用和多功能的序列化程序。我想我做到了。
增量源天生器MemoryPack 完备采取 .NET 6 中增强的增量源天生器[10]。在用法方面,它与 C# 版 MessagePack 没有太大差异,只是将目标类型变动为部分类型。
usingMemoryPack;//SourceGeneratormakesserialize/deserializecode[MemoryPackable]publicpartialclassPerson{publicintAge{get;set;}publicstringName{get;set;}}//usagevarv=newPerson{Age=40,Name="John"};varbin=MemoryPackSerializer.Serialize(v);varval=MemoryPackSerializer.Deserialize<Person>(bin);
源天生器的最大优点是它对 AOT 友好,无需反射即可为每种类型自动天生优化的序列化程序代码,而无需由 IL.Emit 动态天生代码,这是常规做法。这使得利用 Unity 的 IL2CPP 等可以安全地事情。初始启动速率也很快。
源天生器还用作剖析器,因此它可以通过在编辑时发出编译缺点来检测它是否可安全序列化。
请把稳,由于措辞/编译器版本原因,Unity 版本利用旧的源天生器[11]而不是增量源天生器。
C# 的二进制规范MemoryPack 的标语是“零编码”。这不是一个特例,例如,Rust 的紧张二进制序列化器bincode[12] 也有类似的规范。FlatBuffers[13]还可以读取和写入类似于内存数据的内容,而无需解析实现。
但是,与 FlatBuffers 和其他产品不同,MemoryPack 是一种通用的序列化程序,不须要分外类型,并且可以针对 POCO 进行序列化/反序列化。它还具有对架构成员添加和多态性支持 (Union) 的高容忍度的版本掌握。
可变编码与固定编码Int32 是 4 个字节,但在 JSON 中,例如,数字被编码为字符串,可变长度编码为 1~11 个字节(例如,1 或 -2147483648)。许多二进制格式还具有 1 到 5 字节的可变长度编码规范以节省大小。例如,Protocol-buffers 数字类型[14]具有可变长度整数编码,该编码以 7 位存储值,并以 1 位 (varint) 存储是否存在以下的标志。这意味着数字越小,所需的字节就越少。相反,在最坏的情形下,该数字将增长到 5 个字节,大于原来的 4 个字节。MessagePack[15]和CBOR[16]类似地利用可变长度编码进行处理,小数字最小为 1 字节,大数字最大为 5 字节。
这意味着 varint 运行比固定长度情形额外的处理。让我们在详细代码中比较两者。可变长度是 protobuf 中利用的可变 + 之字折线编码(负数和正数组合)。
//FixedencodingstaticvoidWriteFixedInt32(Span<byte>buffer,intvalue){refbytep=refMemoryMarshal.GetReference(buffer);Unsafe.WriteUnaligned(refp,value);}//VarintencodingstaticvoidWriteVarInt32(Span<byte>buffer,intvalue)=>WriteVarInt64(buffer,(long)value);staticvoidWriteVarInt64(Span<byte>buffer,longvalue){refbytep=refMemoryMarshal.GetReference(buffer);ulongn=(ulong)((value<<1)^(value>>63));while((n&~0x7FUL)!=0){Unsafe.WriteUnaligned(refp,(byte)((n&0x7f)|0x80));p=refUnsafe.Add(refp,1);n>>=7;}Unsafe.WriteUnaligned(refp,(byte)n);}
换句话说,固定长度是按原样写出 C# 内存(零编码),很明显,固定长度更快。
当运用于数组时,这一点更加明显。
//https://sharplab.io/Inspect.Heap(newint[]{1,2,3,4,5});
在 C# 中的构造数组中,数据按顺序排列。如果构造没有引用类型(非托管类型)[17]则数据在内存中完备对齐;让我们将代码中的序列化过程与 MessagePack 和 MemoryPack 进行比较。
//Fixed-length(MemoryPack)voidSerialize(int[]value){//Sizecanbecalculatedandallocateinadvancevarsize=(sizeof(int)value.Length)+4;EnsureCapacity(size);//MemoryCopyonceMemoryMarshal.AsBytes(value.AsSpan()).CopyTo(buffer);}// Variable-length(MessagePack)voidSerialize(int[]value){foreach(variteminvalue){//Unknownsize,sochecksizeeachtimesEnsureCapacity();//if(buffer.Length<writeLength)Resize();//VariablelengthencodingperelementWriteVarInt32(item);}}
在固定长度的情形下,可以肃清许多方法调用并且只有一个内存副本。
C# 中的数组不仅是像 int 这样的基元类型,对付具有多个基元的构造也是如此,例如,具有 (float x, float y, float z) 的 Vector3 数组将具有以下内存布局。
浮点数(4 字节)是 MessagePack 中 5 个字节的固定长度。额外的 1 个字节以标识符为前缀,指示值的类型(整数、浮点数、字符串...)。详细来说,[0xca, x, x, x, x, x, x].MemoryPack 格式没有标识符,因此 4 个字节按原样写入。
以 Vector3[10000] 为例,它比基准测试好 50 倍。
//thesefieldsexistsintype//byte[]buffer//intoffsetvoidSerializeMemoryPack(Vector3[]value){//onlydocopyoncevarsize=Unsafe.SizeOf<Vector3>()value.Length;if((buffer.Length-offset)<size){Array.Resize(refbuffer,buffer.Length2);}MemoryMarshal.AsBytes(value.AsSpan()).CopyTo(buffer.AsSpan(0,offset))}voidSerializeMessagePack(Vector3[]value){//Repeatforarraylengthxnumberoffieldsforeach(variteminvalue){//X{//EnsureCapacity//(Actually,createbuffer-linked-listwithbufferWriter.Advance,notResize)if((buffer.Length-offset)<5){Array.Resize(refbuffer,buffer.Length2);}varp=MemoryMarshal.GetArrayDataReference(buffer);Unsafe.WriteUnaligned(refUnsafe.Add(refp,offset),(byte)0xca);Unsafe.WriteUnaligned(refUnsafe.Add(refp,offset+1),item.X);offset+=5;}//Y{if((buffer.Length-offset)<5){Array.Resize(refbuffer,buffer.Length2);}varp=MemoryMarshal.GetArrayDataReference(buffer);Unsafe.WriteUnaligned(refUnsafe.Add(refp,offset),(byte)0xca);Unsafe.WriteUnaligned(refUnsafe.Add(refp,offset+1),item.Y);offset+=5;}//Z{if((buffer.Length-offset)<5){Array.Resize(refbuffer,buffer.Length2);}varp=MemoryMarshal.GetArrayDataReference(buffer);Unsafe.WriteUnaligned(refUnsafe.Add(refp,offset),(byte)0xca);Unsafe.WriteUnaligned(refUnsafe.Add(refp,offset+1),item.Z);offset+=5;}}}
利用 MessagePack,它须要 30000 次方法调用。在该方法中,它会检讨是否有足够的内存进行写入,并在每次完成写入时添加偏移量。
利用 MemoryPack,只有一个内存副本。这实际上会使处理韶光改变一个数量级,这也是本文开头图中 50 倍~100 倍加速的缘故原由。
当然,反序列化过程也是单个副本。
//DeserializeofMemoryPack,onlycopyVector3[]DeserializeMemoryPack(ReadOnlySpan<byte>buffer,intsize){vardest=newVector3[size];MemoryMarshal.Cast<byte,Vector3>(buffer).CopyTo(dest);returndest;}//RequirereadfloatmanytimesinloopVector3[]DeserializeMessagePack(ReadOnlySpan<byte>buffer,intsize){vardest=newVector3[size];for(inti=0;i<size;i++){varx=ReadSingle(buffer);buffer=buffer.Slice(5);vary=ReadSingle(buffer);buffer=buffer.Slice(5);varz=ReadSingle(buffer);buffer=buffer.Slice(5);dest[i]=newVector3(x,y,z);}returndest;}
这是 MessagePack 格式本身的限定,只要遵照规范,速率的巨大差异就无法以任何办法逆转。但是,MessagePack 有一个名为“ext 格式系列”的规范,它许可将这些数组作为其自身规范的一部分进行分外处理。事实上,MessagePack for C# 有一个分外的 Unity 扩展选项,称为 UnsafeBlitResolver,它可以实行上述操作。
但是,大多数人可能不会利用它,也没有人会利用会使 MessagePack 不兼容的专有选项。
因此,对付 MemoryPack,我想要一个默认情形下能供应最佳性能的规范 C#。
字符串优化MemoryPack 有两个字符串规范:UTF8 或 UTF16。由于 C# 字符串是 UTF16,因此将其序列化为 UTF16 可以节省编码/解码为 UTF8 的本钱。
voidEncodeUtf16(stringvalue){varsize=value.Length2;EnsureCapacity(size);//Span<char>->Span<byte>->CopyMemoryMarshal.AsBytes(value.AsSpan()).CopyTo(buffer);}stringDecodeUtf16(ReadOnlySpan<byte>buffer,intlength){ReadOnlySpan<char>src=MemoryMarshal.Cast<byte,char>(buffer).Slice(0,length);returnnewstring(src);}
但是,MemoryPack 默认为 UTF8。这是由于有效负载大小问题;对付 UTF16,ASCII 字符的大小将是原来的两倍,因此选择了 UTF8。
但是,纵然利用 UTF8,MemoryPack 也具有其他序列化程序所没有的一些优化。
//fastvoidWriteUtf8MemoryPack(stringvalue){varsource=value.AsSpan();varmaxByteCount=(source.Length+1)3;EnsureCapacity(maxByteCount);Utf8.FromUtf16(source,dest,outvar_,outvarbytesWritten,replaceInvalidSequences:false);}//slowvoidWriteUtf8StandardSerializer(stringvalue){varmaxByteCount=Encoding.UTF8.GetByteCount(value);EnsureCapacity(maxByteCount);Encoding.UTF8.GetBytes(value,dest);}
var bytes = Encoding.UTF8.GetBytes(value)是绝对的不许可的,字符串写入中不许可 byte[] 分配。许多序列化程序利用 Encoding.UTF8.GetByteCount,但也该当避免它,由于 UTF8 是一种可变长度编码,GetByteCount 完备遍历字符串以打算确切的编码后大小。也便是说,GetByteCount -> GetBytes 遍历字符串两次。
常日,许可序列化程序保留大量缓冲区。因此,MemoryPack 分配三倍的字符串长度,这是 UTF8 编码的最坏情形,以避免双重遍历。在解码的情形下,运用了进一步的分外优化。
//faststringReadUtf8MemoryPack(intutf16Length,intutf8Length){unsafe{fixed(bytep=&buffer){returnstring.Create(utf16Length,((IntPtr)p,utf8Length),static(dest,state)=>{varsrc=MemoryMarshal.CreateSpan(refUnsafe.AsRef<byte>((byte)state.Item1),state.Item2);Utf8.ToUtf16(src,dest,outvarbytesRead,outvarcharsWritten,replaceInvalidSequences:false);});}}}//slowstringReadStandardSerialzier(intutf8Length){returnEncoding.UTF8.GetString(buffer.AsSpan(0,utf8Length));}
常日,要从 byte[] 中获取字符串,我们利用Encoding.UTF8.GetString(buffer)。但同样,UTF8 是一种可变长度编码,我们不知道 UTF16 的长度。UTF8 也是如此。GetString我们须要打算长度为 UTF16 以将其转换为字符串,因此我们在内部扫描字符串两次。在伪代码中,它是这样的:
varlength=CalcUtf16Length(utf8data);varstr=String.Create(length);Encoding.Utf8.DecodeToString(utf8data,str);
范例序列化程序的字符串格式为 UTF8,它不能解码为 UTF16,因此纵然您想要长度为 UTF16 以便作为 C# 字符串进行高效解码,它也不在数据中。
但是,MemoryPack 在标头中记录 UTF16 长度和 UTF8 长度。因此,String.Create<TState>(Int32, TState, SpanAction<Char,TState>) 和 Utf8.ToUtf16的组合为 C# String 供应了最有效的解码。
关于有效负载大小与可变长度编码比较,整数的固定长度编码的大小可能会膨胀。然而,在当代,利用可变长度编码只是为了减小整数的小尺寸是一个缺陷。
由于数据不仅仅是整数,如果真的想减小大小,该当考虑压缩(LZ4[18],ZStandard[19],Brotli[20]等),如果压缩数据,可变长度编码险些没故意义。如果你想更专业和更小,面向列的压缩会给你更大的结果(例如,Apache Parquet[21])。为了与 MemoryPack 实现集成的高效压缩,我目前有 BrotliEncode/Decode 的赞助类作为标准。我还有几个属性,可将分外压缩运用于某些原始列,例如列压缩。
[MemoryPackable]publicpartialclassSample{publicintId{get;set;}[BitPackFormatter]publicbool[]Data{get;set;}[BrotliFormatter]publicbyte[]Payload{get;set;}}
BitPackFormatter表示 bool[],bool 常日为 1 个字节,但由于它被视为 1 位,因此在一个字节中存储八个布尔值。因此,序列化后的大小为 1/8。BrotliFormatter直接应用压缩算法。这实际上比压缩全体文件的性能更好。
这是由于不须要中间副本,压缩过程可以直接应用于序列化数据。Uber 工程博客上的利用CLP 将日志记录本钱降落两个数量级[22]一文中详细先容了通过根据数据以自定义办法运用场置而不是大略的整体压缩来提取性能和压缩率的方法。
利用 .NET7 和 C#11 新功能MemoryPack 在 .NET Standard 2.1 的实现和 .NET 7 的实现中具有略有不同的方法署名。.NET 7 是一种更积极、面向性能的实现,它利用了最新的措辞功能。
首先,序列化程序接口利用静态抽象成员,如下所示:
publicinterfaceIMemoryPackable<T>{//note:serializeparametershouldbe`refreadonly`butcurrentlangspeccannot.//seeproposalhttps://github.com/dotnet/csharplang/issues/6010staticabstractvoidSerialize<TBufferWriter>(refMemoryPackWriter<TBufferWriter>writer,scopedrefT?value)whereTBufferWriter:IBufferWriter<byte>;staticabstractvoidDeserialize(refMemoryPackReaderreader,scopedrefT?value);}
MemoryPack 采取源天生器,并哀求目标类型为[MemoryPackable]public partial class Foo,因此终极的目标类型为
[MemortyPackable]partialclassFoo:IMemoryPackable{staticvoidIMemoryPackable<Foo>.Serialize<TBufferWriter>(refMemoryPackWriter<TBufferWriter>writer,scopedrefFoo?value){}staticvoidIMemoryPackable<Foo>.Deserialize(refMemoryPackReaderreader,scopedrefFoo?value){}}
这避免了通过虚拟方法调用的本钱。
publicvoidWritePackable<T>(scopedinT?value)whereT:IMemoryPackable<T>{//IfTisIMemoryPackable,callstaticmethoddirectlyT.Serialize(refthis,refUnsafe.AsRef(value));}//publicvoidWriteValue<T>(scopedinT?value){//callSerializefrominterfacevirtualmethodIMemoryPackFormatter<T>formatter=MemoryPackFormatterProvider.GetFormatter<T>();formatter.Serialize(refthis,refUnsafe.AsRef(value));}
MemoryPackWriter/MemoryPackReader利用 ref字段。
publicrefstructMemoryPackWriter<TBufferWriter>whereTBufferWriter:IBufferWriter<byte>{refTBufferWriterbufferWriter;refbytebufferReference;intbufferLength;
换句话说,ref byte bufferReference,int bufferLength的组合是Span<byte>的内联。此外,通过接管 TBufferWriter 作为 ref TBufferWriter,现在可以安全地接管和调用可变构造 TBufferWriter:IBufferWrite<byte>。
//internallyMemoryPackusessomestructbuffer-writersstructBrotliCompressor:IBufferWriter<byte>structFixedArrayBufferWriter:IBufferWriter<byte>
针对所有类型的类型进行优化
例如,对付通用实现,凑集可以序列化/反序列化为 IEnumerable<T>,但 MemoryPack 为所有类型的供应单独的实现。为大略起见,List<T> 可以处理为:
publicvoidSerialize(refMemoryPackWriterwriter,IEnumerable<T>value){foreach(variteminsource){writer.WriteValue(item);}}publicvoidSerialize(refMemoryPackWriterwriter,List<T>value){foreach(variteminsource){writer.WriteValue(item);}}
这两个代码看起来相同,但实行完备不同:foreach to IEnumerable<T> 检索IEnumerator<T>,而 foreach to List<T>检索构造List<T>.Enumerator,y 一个优化的专用构造。
但是,MemoryPack 进一步优化了它。
publicsealedclassListFormatter<T>:MemoryPackFormatter<List<T?>>{publicoverridevoidSerialize<TBufferWriter>(refMemoryPackWriter<TBufferWriter>writer,scopedrefList<T?>?value){if(value==null){writer.WriteNullCollectionHeader();return;}writer.WriteSpan(CollectionsMarshal.AsSpan(value));}}//MemoryPackWriter.WriteSpan[MethodImpl(MethodImplOptions.AggressiveInlining)]publicvoidWriteSpan<T>(scopedSpan<T?>value){if(!RuntimeHelpers.IsReferenceOrContainsReferences<T>()){DangerousWriteUnmanagedSpan(value);return;}varformatter=GetFormatter<T>();WriteCollectionHeader(value.Length);for(inti=0;i<value.Length;i++){formatter.Serialize(refthis,refvalue[i]);}}//MemoryPackWriter.DangerousWriteUnmanagedSpan[MethodImpl(MethodImplOptions.AggressiveInlining)]publicvoidDangerousWriteUnmanagedSpan<T>(scopedSpan<T>value){if(value.Length==0){WriteCollectionHeader(0);return;}varsrcLength=Unsafe.SizeOf<T>()value.Length;varallocSize=srcLength+4;refvardest=refGetSpanReference(allocSize);refvarsrc=refUnsafe.As<T,byte>(refMemoryMarshal.GetReference(value));Unsafe.WriteUnaligned(refdest,value.Length);Unsafe.CopyBlockUnaligned(refUnsafe.Add(refdest,4),refsrc,(uint)srcLength);Advance(allocSize);}
来自 .NET 5 的 CollectionsMarshal.AsSpan 是列举 List<T> 的最佳办法。此外,如果可以得到 Span<T>,则只能在 List<int>或 List<Vector3>的情形下通过复制来处理。
在反序列化的情形下,也有一些有趣的优化。首先,MemoryPack 的反序列化接管引用 T?值,如果值为 null,则如果通报该值,它将覆盖内部天生的工具(就像普通序列化程序一样)。这许可在反序列化期间零分配新工具创建。在List<T> 的情形下,也可以通过调用 Clear() 来重用凑集。
然后,通过进行分外的 Span 调用,它全部作为 Span 处理,避免了List<T>.Add的额外开销。
publicsealedclassListFormatter<T>:MemoryPackFormatter<List<T?>>{publicoverridevoidDeserialize(refMemoryPackReaderreader,scopedrefList<T?>?value){if(!reader.TryReadCollectionHeader(outvarlength)){value=null;return;}if(value==null){value=newList<T?>(length);}elseif(value.Count==length){value.Clear();}varspan =CollectionsMarshalEx.CreateSpan(value,length);reader.ReadSpanWithoutReadLengthHeader(length,refspan);}}internalstaticclassCollectionsMarshalEx{///<summary>///similarasAsSpanbutmodifysizetocreatefixed-sizespan.///</summary>publicstaticSpan<T?>CreateSpan<T>(List<T?>list,intlength){list.EnsureCapacity(length);refvarview=refUnsafe.As<List<T?>,ListView<T?>>(reflist);view._size=length;returnview._items.AsSpan(0,length);}//NOTE:Thesestructuredepndenton.NET7,ifchanged,requiretokeepsamestructure.internalsealedclassListView<T>{publicT[]_items;publicint_size;publicint_version;}}//MemoryPackReader.ReadSpanWithoutReadLengthHeaderpublicvoidReadSpanWithoutReadLengthHeader<T>(intlength,scopedrefSpan<T?>value){if(length==0){value=Array.Empty<T>();return;}if(!RuntimeHelpers.IsReferenceOrContainsReferences<T>()){if(value.Length!=length){value=AllocateUninitializedArray<T>(length);}varbyteCount=lengthUnsafe.SizeOf<T>();refvarsrc=refGetSpanReference(byteCount);refvardest=refUnsafe.As<T,byte>(refMemoryMarshal.GetReference(value)!);Unsafe.CopyBlockUnaligned(refdest,refsrc,(uint)byteCount);Advance(byteCount);}else{if(value.Length!=length){value=newT[length];}varformatter=GetFormatter<T>();for(inti=0;i<length;i++){formatter.Deserialize(refthis,refvalue[i]);}}}
EnsurceCapacity(capacity),可以预先扩展保存 List<T> 的内部数组的大小。这避免了每次都须要内部放大/复制。
但是 CollectionsMarshal.AsSpan,您将得到长度为 0 的 Span,由于内部大小不会变动。如果我们有 CollectionMarshals.AsMemory,我们可以利用 MemoryMarshal.TryGetArray 组合从那里获取原始数组,但不幸的是,没有办法从 Span 获取原始数组。因此,我逼迫类型构造与 Unsafe.As 匹配并变动List<T>._size,我能够得到扩展的内部数组。
这样,我们可以以仅复制的办法优化非托管类型,并避免 List<T>.Add(每次检讨数组大小),并通过Span<T>[index] 打包值,这比传统序列化、反序列化程序性能要高得多。
虽然对List<T>的优化具有代表性,但要先容的还有太多其他类型,所有类型都经由仔细审查,并且对每种类型都运用了最佳优化。
Serialize 接管 IBufferWriter<byte> 作为其本机构造,反序列化接管 ReadOnlySpan<byte> 和 ReadOnlySequence<byte>。
这是由于System.IO.Pipelines[23] 须要这些类型。换句话说,由于它是 ASP .NET Core 的做事器 (Kestrel) 的根本,因此通过直接连接到它,您可以期待更高性能的序列化。
IBufferWriter<byte> 特殊主要,由于它可以直接写入缓冲区,从而在序列化过程中实现零拷贝。对 IBufferWriter<byte> 的支持是当代序列化程序的先决条件,由于它供应比利用 byte[] 或 Stream 更高的性能。开头图表中的序列化程序(System.Text.Json,protobuf-net,Microsoft.Orleans.Serialization,MessagePack for C#和 MemoryPack)支持它。
MessagePack 与 MemoryPackMessagePack for C# 非常易于利用,并且具有出色的性能。特殊是,以下几点比 MemoryPack 更好
出色的跨措辞兼容性JSON 兼容性(尤其是字符串键)和人类可读性默认完美版本容错工具和匿名类型的序列化动态反序列化嵌入式 LZ4 压缩久经磨练的稳定性MemoryPack 默认为有限版本容错,完全版容错选项的性能略低。此外,由于它是原始格式,以是唯一支持的其他措辞是 TypeScript。此外,二进制文件本身不会见告它是什么数据,由于它须要 C# 架构。
但是,它在以下方面优于 MessagePack。
性能,尤其是对付非托管类型数组易于利用的 AOT 支持扩展多态性(联合)布局方法支持循环引用覆盖反序列化打字稿代码天生灵巧的基于属性的自定义格式化程序在我个人看来,如果你在只有 C#的环境中,我会选择 MemoryPack。但是,有限版本容错有其怪癖,该当事先理解它。MessagePack for C# 仍旧是一个不错的选择,由于它大略易用。
MemoryPack 不是一个只关注性能的实验性序列化程序,而且还旨在成为一个实用的序列化程序。为此,我还以 MessagePack for C# 的履历为根本,供应了许多功能。
支持当代 I/O API(IBufferWriter<byte>,ReadOnlySpan<byte>, ReadOnlySequence<byte>)基于本机 AOT 友好的源天生器的代码天生,没有动态代码天生(IL.Emit)无反射非泛型 API反序列化到现有实例多态性(联合)序列化有限的版本容限(快速/默认)和完全的版本容错支持循环引用序列化基于管道写入器/读取器的流式序列化TypeScript 代码天生和核心格式化程序 ASP.NETUnity(2021.3) 通过 .NET 源天生器支持 IL2CPP我们操持进一步扩展可用功能的范围,例如对MasterMemory 的 MemoryPack[24]支持和对 MagicOnion[25]的序列化程序变动支持等。我们将自己定位为Cysharp C# 库[26]生态系统的核心。我们将付出很多努力来种下这一棵树,以是对付初学者来说,请考试测验一下我们的库!
已得到原作者独家授权
原文版权:neuecc
翻译版权:InCerry
原文链接:https://neuecc.medium.com/how-to-make-the-fastest-net-serializer-with-net-7-c-11-case-of-memorypack-ad28c0366516
参考资料[1]
MemoryPack: https://github.com/Cysharp/MemoryPack
[2]
MessagePack for C#: https://github.com/neuecc/MessagePack-CSharp/
[3]
Utf8Json: https://github.com/neuecc/Utf8Json
[4]
SignalR MessagePack Hub: https://learn.microsoft.com/en-us/aspnet/core/signalr/messagepackhubprotocol
[5]
ZeroFormatter: https://github.com/neuecc/ZeroFormatter
[6]
Utf8Json: https://github.com/neuecc/Utf8Json
[7]
MagicOnion: https://github.com/Cysharp/MagicOnion
[8]
MasterMemory: https://github.com/Cysharp/MasterMemory
[9]
AlterNats: https://github.com/Cysharp/AlterNats
[10]
增量源天生器: https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.md
[11]
旧的源天生器: https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview
[12]
bincode: https://github.com/bincode-org/bincode
[13]
FlatBuffers: https://github.com/google/flatbuffers
[14]
Protocol-buffers数字类型: https://developers.google.com/protocol-buffers/docs/encoding#varints
[15]
MessagePack: https://github.com/msgpack/msgpack/blob/master/spec.md
[16]
CBOR: https://cbor.io/
[17]
构造没有引用类型(非托管类型): https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/unmanaged-types
[18]
LZ4: https://github.com/lz4/lz4
[19]
ZStandard: http://facebook.github.io/zstd/
[20]
Brotli: https://github.com/google/brotli
[21]
Apache Parquet: https://parquet.apache.org/
[22]
CLP 将日志记录本钱降落两个数量级: https://www.uber.com/en-DE/blog/reducing-logging-cost-by-two-orders-of-magnitude-using-clp/
[23]
System.IO.Pipelines: https://learn.microsoft.com/en-us/dotnet/standard/io/pipelines
[24]
MasterMemory的MemoryPack: https://github.com/Cysharp/MasterMemory
[25]
对MagicOnion: https://github.com/Cysharp/MagicOnion
[26]
Cysharp C# 库: https://github.com/Cysharp/
本站所发布的文字与图片素材为非商业目的改编或整理,版权归原作者所有,如侵权或涉及违法,请联系我们删除,如需转载请保留原文地址:http://www.baanla.com/lz/zxsj/67592.html
Copyright 2005-20203 www.baidu.com 版权所有 | 琼ICP备2023011765号-4 | 统计代码
声明:本站所有内容均只可用于学习参考,信息与图片素材来源于互联网,如内容侵权与违规,请与本站联系,将在三个工作日内处理,联系邮箱:123456789@qq.com