使用.NET7和C#11打造最快的序列化程序-以MemoryPack为例

译者注

本文是一篇不可多得的好文,MemoryPack 的作者 neuecc 大佬通过本文解释了他是如何将序列化程序性能提升到极致的;其中从很多方面(可变长度、字符串、集合等)解释了一些性能优化的技巧,值得每一个开发人员学习,特别是框架的开发人员的学习,一定能让大家获益匪浅。

由于公众号排版原因,译者建议大家在桌面端阅读本文,手机阅读体验并不是很好。

简介

我发布了一个名为MemoryPack[1] 的新序列化程序,这是一种特定于 C# 的新序列化程序,其执行速度比其他序列化程序快得多。

5dd8958f643db7f5639539918e61fdb8.png

与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 没有太大区别,只是将目标类型更改为部分类型。

using MemoryPack;// Source Generator makes serialize/deserialize code
[MemoryPackable]
public partial class Person
{public int Age { get; set; }public string Name { get; set; }
}// usage
var v = new Person { Age = 40, Name = "John" };var bin = MemoryPackSerializer.Serialize(v);
var val = MemoryPackSerializer.Deserialize<Person>(bin);

源生成器的最大优点是它对 AOT 友好,无需反射即可为每种类型自动生成优化的序列化程序代码,而无需由 IL.Emit 动态生成代码,这是常规做法。这使得使用 Unity 的 IL2CPP 等可以安全地工作。初始启动速度也很快。

3fbd86c688f50dd8712b780c5a2f6c00.png

源生成器还用作分析器,因此它可以通过在编辑时发出编译错误来检测它是否可安全序列化。

请注意,由于语言/编译器版本原因,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 中使用的可变 + 之字折线编码(负数和正数组合)。

// Fixed encoding
static void WriteFixedInt32(Span<byte> buffer, int value)
{ref byte p = ref MemoryMarshal.GetReference(buffer);Unsafe.WriteUnaligned(ref p, value);
}// Varint encoding
static void WriteVarInt32(Span<byte> buffer, int value) => WriteVarInt64(buffer, (long)value);static void WriteVarInt64(Span<byte> buffer, long value)
{ref byte p = ref MemoryMarshal.GetReference(buffer);ulong n = (ulong)((value << 1) ^ (value >> 63));while ((n & ~0x7FUL) != 0){Unsafe.WriteUnaligned(ref p, (byte)((n & 0x7f) | 0x80));p = ref Unsafe.Add(ref p, 1);n >>= 7;}Unsafe.WriteUnaligned(ref p, (byte)n);
}

换句话说,固定长度是按原样写出 C# 内存(零编码),很明显,固定长度更快。

当应用于数组时,这一点更加明显。

// https://sharplab.io/
Inspect.Heap(new int[]{ 1, 2, 3, 4, 5 });
b0a4ca0e2aa71bac23f80190f382627d.png

在 C# 中的结构数组中,数据按顺序排列。如果结构没有引用类型(非托管类型)[17]则数据在内存中完全对齐;让我们将代码中的序列化过程与 MessagePack 和 MemoryPack 进行比较。

// Fixed-length(MemoryPack)
void Serialize(int[] value)
{// Size can be calculated and allocate in advancevar size = (sizeof(int) * value.Length) + 4;EnsureCapacity(size);// MemoryCopy onceMemoryMarshal.AsBytes(value.AsSpan()).CopyTo(buffer);
}// Variable-length(MessagePack)
void Serialize(int[] value)
{foreach (var item in value){// Unknown size, so check size each timesEnsureCapacity(); // if (buffer.Length < writeLength) Resize();// Variable length encoding per elementWriteVarInt32(item);}
}

在固定长度的情况下,可以消除许多方法调用并且只有一个内存副本。

C# 中的数组不仅是像 int 这样的基元类型,对于具有多个基元的结构也是如此,例如,具有 (float x, float y, float z) 的 Vector3 数组将具有以下内存布局。

1bea7599e6494a7b7e29c65649d345cc.png

浮点数(4 字节)是 MessagePack 中 5 个字节的固定长度。额外的 1 个字节以标识符为前缀,指示值的类型(整数、浮点数、字符串...)。具体来说,[0xca, x, x, x, x, x, x].MemoryPack 格式没有标识符,因此 4 个字节按原样写入。

以 Vector3[10000] 为例,它比基准测试好 50 倍。

// these fields exists in type
// byte[] buffer
// int offsetvoid SerializeMemoryPack(Vector3[] value)
{// only do copy oncevar size = Unsafe.SizeOf<Vector3>() * value.Length;if ((buffer.Length - offset) < size){Array.Resize(ref buffer, buffer.Length * 2);}MemoryMarshal.AsBytes(value.AsSpan()).CopyTo(buffer.AsSpan(0, offset))
}void SerializeMessagePack(Vector3[] value)
{// Repeat for array length x number of fieldsforeach (var item in value){// X{// EnsureCapacity// (Actually, create buffer-linked-list with bufferWriter.Advance, not Resize)if ((buffer.Length - offset) < 5){Array.Resize(ref buffer, buffer.Length * 2);}var p = MemoryMarshal.GetArrayDataReference(buffer);Unsafe.WriteUnaligned(ref Unsafe.Add(ref p, offset), (byte)0xca);Unsafe.WriteUnaligned(ref Unsafe.Add(ref p, offset + 1), item.X);offset += 5;}// Y{if ((buffer.Length - offset) < 5){Array.Resize(ref buffer, buffer.Length * 2);}var p = MemoryMarshal.GetArrayDataReference(buffer);Unsafe.WriteUnaligned(ref Unsafe.Add(ref p, offset), (byte)0xca);Unsafe.WriteUnaligned(ref Unsafe.Add(ref p, offset + 1), item.Y);offset += 5;}// Z{if ((buffer.Length - offset) < 5){Array.Resize(ref buffer, buffer.Length * 2);}var p = MemoryMarshal.GetArrayDataReference(buffer);Unsafe.WriteUnaligned(ref Unsafe.Add(ref p, offset), (byte)0xca);Unsafe.WriteUnaligned(ref Unsafe.Add(ref p, offset + 1), item.Z);offset += 5;}}
}

使用 MessagePack,它需要 30000 次方法调用。在该方法中,它会检查是否有足够的内存进行写入,并在每次完成写入时添加偏移量。

使用 MemoryPack,只有一个内存副本。这实际上会使处理时间改变一个数量级,这也是本文开头图中 50 倍~100 倍加速的原因。

当然,反序列化过程也是单个副本。

// Deserialize of MemoryPack, only copy
Vector3[] DeserializeMemoryPack(ReadOnlySpan<byte> buffer, int size)
{var dest = new Vector3[size];MemoryMarshal.Cast<byte, Vector3>(buffer).CopyTo(dest);return dest;
}// Require read float many times in loop
Vector3[] DeserializeMessagePack(ReadOnlySpan<byte> buffer, int size)
{var dest = new Vector3[size];for (int i = 0; i < size; i++){var x = ReadSingle(buffer);buffer = buffer.Slice(5);var y = ReadSingle(buffer);buffer = buffer.Slice(5);var z = ReadSingle(buffer);buffer = buffer.Slice(5);dest[i] = new Vector3(x, y, z);}return dest;
}

这是 MessagePack 格式本身的限制,只要遵循规范,速度的巨大差异就无法以任何方式逆转。但是,MessagePack 有一个名为“ext 格式系列”的规范,它允许将这些数组作为其自身规范的一部分进行特殊处理。事实上,MessagePack for C# 有一个特殊的 Unity 扩展选项,称为 UnsafeBlitResolver,它可以执行上述操作。

但是,大多数人可能不会使用它,也没有人会使用会使 MessagePack 不兼容的专有选项。

因此,对于 MemoryPack,我想要一个默认情况下能提供最佳性能的规范 C#。

字符串优化

MemoryPack 有两个字符串规范:UTF8 或 UTF16。由于 C# 字符串是 UTF16,因此将其序列化为 UTF16 可以节省编码/解码为 UTF8 的成本。

void EncodeUtf16(string value)
{var size = value.Length * 2;EnsureCapacity(size);// Span<char> -> Span<byte> -> CopyMemoryMarshal.AsBytes(value.AsSpan()).CopyTo(buffer);
}string DecodeUtf16(ReadOnlySpan<byte> buffer, int length)
{ReadOnlySpan<char> src = MemoryMarshal.Cast<byte, char>(buffer).Slice(0, length);return new string(src);
}

但是,MemoryPack 默认为 UTF8。这是因为有效负载大小问题;对于 UTF16,ASCII 字符的大小将是原来的两倍,因此选择了 UTF8。

但是,即使使用 UTF8,MemoryPack 也具有其他序列化程序所没有的一些优化。

// fast
void WriteUtf8MemoryPack(string value)
{var source = value.AsSpan();var maxByteCount = (source.Length + 1) * 3;EnsureCapacity(maxByteCount);Utf8.FromUtf16(source, dest, out var _, out var bytesWritten, replaceInvalidSequences: false);
}// slow
void WriteUtf8StandardSerializer(string value)
{var maxByteCount = 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 编码的最坏情况,以避免双重遍历。在解码的情况下,应用了进一步的特殊优化。

// fast
string ReadUtf8MemoryPack(int utf16Length, int utf8Length)
{unsafe{fixed (byte* p = &buffer){return string.Create(utf16Length, ((IntPtr)p, utf8Length), static (dest, state) =>{var src = MemoryMarshal.CreateSpan(ref Unsafe.AsRef<byte>((byte*)state.Item1), state.Item2);Utf8.ToUtf16(src, dest, out var bytesRead, out var charsWritten, replaceInvalidSequences: false);});}}
}// slow
string ReadStandardSerialzier(int utf8Length)
{return Encoding.UTF8.GetString(buffer.AsSpan(0, utf8Length));
}

通常,要从 byte[] 中获取字符串,我们使用Encoding.UTF8.GetString(buffer)。但同样,UTF8 是一种可变长度编码,我们不知道 UTF16 的长度。UTF8 也是如此。GetString我们需要计算长度为 UTF16 以将其转换为字符串,因此我们在内部扫描字符串两次。在伪代码中,它是这样的:

var length = CalcUtf16Length(utf8data);
var str = 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]
public partial class Sample
{public int Id { get; set; }[BitPackFormatter]public bool[] Data { get; set; }[BrotliFormatter]public byte[] Payload { get; set; }
}

BitPackFormatter表示 bool[],bool 通常为 1 个字节,但由于它被视为 1 位,因此在一个字节中存储八个布尔值。因此,序列化后的大小为 1/8。BrotliFormatter直接应用压缩算法。这实际上比压缩整个文件的性能更好。

10fdacdf3c7f74a6cea122c0b7945bba.png

这是因为不需要中间副本,压缩过程可以直接应用于序列化数据。Uber 工程博客上的使用CLP 将日志记录成本降低两个数量级[22]一文中详细介绍了通过根据数据以自定义方式应用处理而不是简单的整体压缩来提取性能和压缩率的方法。

使用 .NET7 和 C#11 新功能

MemoryPack 在 .NET Standard 2.1 的实现和 .NET 7 的实现中具有略有不同的方法签名。.NET 7 是一种更积极、面向性能的实现,它利用了最新的语言功能。

首先,序列化程序接口利用静态抽象成员,如下所示:

public interface IMemoryPackable<T>
{// note: serialize parameter should be `ref readonly` but current lang spec can not.// see proposal https://github.com/dotnet/csharplang/issues/6010static abstract void Serialize<TBufferWriter>(ref MemoryPackWriter<TBufferWriter> writer, scoped ref T? value)where TBufferWriter : IBufferWriter<byte>;static abstract void Deserialize(ref MemoryPackReader reader, scoped ref T? value);
}

MemoryPack 采用源生成器,并要求目标类型为[MemoryPackable]public partial class Foo,因此最终的目标类型为

[MemortyPackable]
partial class Foo : IMemoryPackable
{static void IMemoryPackable<Foo>.Serialize<TBufferWriter>(ref MemoryPackWriter<TBufferWriter> writer, scoped ref Foo? value){}static void IMemoryPackable<Foo>.Deserialize(ref MemoryPackReader reader, scoped ref Foo? value){}
}

这避免了通过虚拟方法调用的成本。

public void WritePackable<T>(scoped in T? value)where T : IMemoryPackable<T>
{// If T is IMemoryPackable, call static method directlyT.Serialize(ref this, ref Unsafe.AsRef(value));
}//
public void WriteValue<T>(scoped in T? value)
{// call Serialize from interface virtual methodIMemoryPackFormatter<T> formatter = MemoryPackFormatterProvider.GetFormatter<T>();formatter.Serialize(ref this, ref Unsafe.AsRef(value));
}

MemoryPackWriter/MemoryPackReader使用 ref字段。

public ref struct MemoryPackWriter<TBufferWriter>where TBufferWriter : IBufferWriter<byte>
{ref TBufferWriter bufferWriter;ref byte bufferReference;int bufferLength;

换句话说,ref byte bufferReferenceint bufferLength的组合是Span<byte>的内联。此外,通过接受 TBufferWriter 作为 ref TBufferWriter,现在可以安全地接受和调用可变结构 TBufferWriter:IBufferWrite<byte>

// internally MemoryPack uses some struct buffer-writers
struct BrotliCompressor : IBufferWriter<byte>
struct FixedArrayBufferWriter : IBufferWriter<byte>

针对所有类型的类型进行优化

例如,对于通用实现,集合可以序列化/反序列化为 IEnumerable<T>,但 MemoryPack 为所有类型的提供单独的实现。为简单起见,List<T> 可以处理为:

public void Serialize(ref MemoryPackWriter writer, IEnumerable<T> value)
{foreach(var item in source){writer.WriteValue(item);}
}public void Serialize(ref MemoryPackWriter writer, List<T> value)
{foreach(var item in source){writer.WriteValue(item);}
}

这两个代码看起来相同,但执行完全不同:foreach to IEnumerable<T> 检索IEnumerator<T>,而 foreach to List<T>检索结构List<T>.Enumerator,y 一个优化的专用结构。

但是,MemoryPack 进一步优化了它。

public sealed class ListFormatter<T> : MemoryPackFormatter<List<T?>>
{public override void Serialize<TBufferWriter>(ref MemoryPackWriter<TBufferWriter> writer, scoped ref List<T?>? value){if (value == null){writer.WriteNullCollectionHeader();return;}writer.WriteSpan(CollectionsMarshal.AsSpan(value));}
}// MemoryPackWriter.WriteSpan
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteSpan<T>(scoped Span<T?> value)
{if (!RuntimeHelpers.IsReferenceOrContainsReferences<T>()){DangerousWriteUnmanagedSpan(value);return;}var formatter = GetFormatter<T>();WriteCollectionHeader(value.Length);for (int i = 0; i < value.Length; i++){formatter.Serialize(ref this, ref value[i]);}
}// MemoryPackWriter.DangerousWriteUnmanagedSpan
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void DangerousWriteUnmanagedSpan<T>(scoped Span<T> value)
{if (value.Length == 0){WriteCollectionHeader(0);return;}var srcLength = Unsafe.SizeOf<T>() * value.Length;var allocSize = srcLength + 4;ref var dest = ref GetSpanReference(allocSize);ref var src = ref Unsafe.As<T, byte>(ref MemoryMarshal.GetReference(value));Unsafe.WriteUnaligned(ref dest, value.Length);Unsafe.CopyBlockUnaligned(ref Unsafe.Add(ref dest, 4), ref src, (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的额外开销。

public sealed class ListFormatter<T> : MemoryPackFormatter<List<T?>>
{public override void Deserialize(ref MemoryPackReader reader, scoped ref List<T?>? value){if (!reader.TryReadCollectionHeader(out var length)){value = null;return;}if (value == null){value = new List<T?>(length);}else if (value.Count == length){value.Clear();}var span = CollectionsMarshalEx.CreateSpan(value, length);reader.ReadSpanWithoutReadLengthHeader(length, ref span);}
}internal static class CollectionsMarshalEx
{/// <summary>/// similar as AsSpan but modify size to create fixed-size span./// </summary>public static Span<T?> CreateSpan<T>(List<T?> list, int length){list.EnsureCapacity(length);ref var view = ref Unsafe.As<List<T?>, ListView<T?>>(ref list);view._size = length;return view._items.AsSpan(0, length);}// NOTE: These structure depndent on .NET 7, if changed, require to keep same structure.internal sealed class ListView<T>{public T[] _items;public int _size;public int _version;}
}// MemoryPackReader.ReadSpanWithoutReadLengthHeader
public void ReadSpanWithoutReadLengthHeader<T>(int length, scoped ref Span<T?> value)
{if (length == 0){value = Array.Empty<T>();return;}if (!RuntimeHelpers.IsReferenceOrContainsReferences<T>()){if (value.Length != length){value = AllocateUninitializedArray<T>(length);}var byteCount = length * Unsafe.SizeOf<T>();ref var src = ref GetSpanReference(byteCount);ref var dest = ref Unsafe.As<T, byte>(ref MemoryMarshal.GetReference(value)!);Unsafe.CopyBlockUnaligned(ref dest, ref src, (uint)byteCount);Advance(byteCount);}else{if (value.Length != length){value = new T[length];}var formatter = GetFormatter<T>();for (int i = 0; i < length; i++){formatter.Deserialize(ref this, ref value[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 与 MemoryPack

MessagePack 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.NET

  • Unity(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.mzph.cn/news/280680.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

永不丢失照片:防弹照片备份的完整指南

There’s nothing as precious and irreplaceable as your personal photos and, with a little forethought and planning, there’s no reason to ever feel the heartbreak of losing even a single one of them to theft, broken devices, or disaster. 没有比您的个人照片…

MySQL InnoDB存储引擎

呵呵哒。。。 MySQL体系结构和存储引擎 首先要搞懂的是什么是数据库&#xff0c;什么是数据库实例。 数据库&#xff1a;物理操作系统文件或其他形式文件类型的集合。 实例&#xff1a;MySQL数据库由后台线程以及一个共享内存区组成&#xff0c;实例才是真正对数据库进行操作的…

Blazor学习之旅 (8) MudBlazor组件库介绍

【Blazor】| 总结/Edison Zhou大家好&#xff0c;我是Edison。为了实现一个Web应用系统&#xff0c;需要有个看起来不丑的UI&#xff0c;而对于.NET程序员来说要做全栈开发还是有点难&#xff0c;而本篇介绍的这个UI组件库正好可以帮助我们解决这个问题&#xff01;MudBlaozr是…

棉花糖多少钱_如何在6.0棉花糖及更高版本中访问Android的正在运行的应用程序列表...

棉花糖多少钱In Android 5.x and below, accessing your list of running apps was simple—you’d jump into Settings > Apps > Running. Easy! In Android 6.0, however, Google moved this setting. It’s still not super difficult to find, but it’s a little tr…

C# 程序图标设置/winform 图标

一、目的、实际情况 1.编写一个winform 程序&#xff0c;发现有一个图标非常有意义。区分其他程序&#xff0c;以及感觉在做产品而不是写代码。 2.添加图标图片发现&#xff0c;需要用ico格式。在线转换&#xff08;某度搜索&#xff09;还是不靠谱。要微信登陆&#xff0c;登…

数字化转型,究竟在“转”什么?

这是头哥侃码的第265篇原创「头哥唠B唠」这个栏目已经持续了几个月了&#xff0c;没想到还在继续进行&#xff0c;并收获了很多朋友们的喜爱。非常感谢大家的支持&#xff01;在上次的直播中&#xff0c;我找来了我的老熟人们。一个是右军老师&#xff0c;之前 APISIX 的很多内…

Crash 的文明世界

题目描述 给一棵树&#xff0c;求以每个点为根时下列式子的值。 题解 当k1时这就是一个经典的换根dp问题。 所以这道题还是要用换根dp解决。 部分分做法&#xff1a; 考虑转移时是这样的一个形式(图是抄的)。 用二项式定理展开就可以nk2做了。 观察到结果是一个xk的形式。 然后…

wampServer配置WWW根目录遇到的坑

直接在官网下载之后开始安装&#xff0c;一切正常 打开使用&#xff0c;一切正常 设置WWW目录。坑了一波 按照的都是百度上的教程&#xff0c;设置httpd.conf 这里配置之后网页访问127.0.0.1 还是localhost都还是原始的www目录 后来 我发现了这里 是配置虚拟URL的地方。以上是正…

windows安装程序创建_如何在Windows上创建已安装程序的列表

windows安装程序创建Reinstalling Windows is a good way to fix serious problems with your computer, or just to get a fresh slate. But before you reinstall Windows, you should make a list of programs you currently have installed on your PC so you know what yo…

实现一个更新所有 dotnet tool 的 dotnet tool

实现一个更新所有 dotnet tool 的 dotnet toolIntrodotnet tool 是从 .NET Core 2.1 开始支持的命令行工具&#xff0c;在使用 dotnet tool 比较多了的时候&#xff0c;想要更新所有的 dotnet tool 就比较麻烦&#xff0c;而目前 .NET SDK 还不支持&#xff0c;也有一些人希望能…

[AHOI2009]飞行棋 BZOJ1800

题目描述 给出圆周上的若干个点&#xff0c;已知点与点之间的弧长&#xff0c;其值均为正整数&#xff0c;并依圆周顺序排列。 请找出这些点中有没有可以围成矩形的&#xff0c;并希望在最短时间内找出所有不重复矩形。 输入输出格式 输入格式&#xff1a;第一行为正整数N&…

webapi+Quartz.NET解决若干定时程序同时运行的问题

项目现状&#xff1a; 有若干定时程序需要自启动运行&#xff0c;为了简便程序部署等问题&#xff0c;采取这种办法把定时程序集中管理到webapi中跟随api发布 代码架构介绍&#xff1a; 新建一个类库&#xff0c;类库引用Quartz&#xff08;Quartz.2.3.2&#xff09;&#xff0…

mac恢复iphone_免费下载:旧Mac和iPhone壁纸的令人震惊的完整档案

mac恢复iphoneLove or hate Apple, you’ve got to admit: their background images are consistently stunning. Now you can download all of them. 爱或恨苹果&#xff0c;您必须承认&#xff1a;它们的背景图像始终令人赞叹。 现在&#xff0c;您可以下载所有这些文件。 A …

Magicodes.IE 2.7.1发布

2.7.12022.12.01Magicodes.IE.EPPlus默认添加SkiaSharp.NativeAssets.Linux.NoDependencies包&#xff0c;以便于在Linux环境下使用导入验证支持将错误数据通过Stream的方式返回&#xff0c;感谢sampsonye &#xff08;见pr#466&#xff09;2.7.02022.11.07添加SkiaSharp移除Si…

Comcast以纯文本泄露客户Wi-Fi登录信息,立即更改密码

A Comcast Xfinity website was leaking Wi-Fi names and passwords, meaning now is a good time to change your Wi-Fi passcode. Comcast Xfinity网站泄漏了Wi-Fi名称和密码&#xff0c;这意味着现在是更改Wi-Fi密码的好时机。 The site, intended to help new customers se…

龙芯上跑WTM,为国产化做点贡献

点击上方蓝字关注我哦“信创”&#xff0c;是一项国家战略&#xff0c;即信息技术应用创新产业&#xff0c;它是数据安全、网络安全的基础&#xff0c;也是新基建的重要组成部分。信创从名称上来看本意指向创新&#xff0c;但是自从漂亮国亲手撕碎了“科技没有国界”的谎言之后…

PHP安装之configure的配置参数

1、生成环境安装配置如下 要求安装如下库&#xff1a; imagickgdmysqlmysqlimysqlndphalconPharsoapsocketsxwebxsvczipzlib 具体查看 vim php-config 就可以知道是如何配置的 --prefix/home/php --with-config-file-path/home/php/etc --with-mysql --with-pdo-oci --with-ope…

Django05: 请求生命周期流程图/路由层

请求生命周期流程图 扩展知识&#xff1a; 缓存数据库 路由层 路由匹配 url(r^test/, views.test), 1. 第一个参数是正则匹配。 只要第一个匹配了&#xff0c;就不会执行下面。 输入url会默认加斜杠&#xff0c;django会重定向 a. 一次匹配不行 b. url再加斜杠匹配 可以…

facebook 分享页面_Facebook个人资料,页面和组之间有什么区别?

facebook 分享页面Facebook is used by a lot of different people for a lot of different things, so it’s only natural that Facebook would have different sets of features for each of them. There are three main ways you can use Facebook: with a regular Profile…

一句话设计原则

面向对象的可复用设计&#xff08; Object Oriented Design / OOD&#xff09; 1. 开闭原则 (Open Closed Principle) 对扩展开放&#xff0c;对修改关闭 2. 里氏代换原则(LSP) 1.可以使用基类的地方&#xff0c;其子类必然也能使用 2.并且原功能不会受到任何影响 -- 经典案例,…