So erstellen Sie den schnellsten .NET-Serializer mit .NET 7 / C# 11, bei MemoryPack
Ich habe einen neuen Serializer namens MemoryPack veröffentlicht, einen neuen C#-spezifischen Serializer, der viel schneller arbeitet als andere Serializer.
Im Vergleich zu MessagePack für C# , einem schnellen binären Serialisierer, ist die Leistung bei Standardobjekten um ein Vielfaches höher und bei optimalen Daten sogar 50- bis 100-mal schneller. Die beste Unterstützung ist .NET 7, unterstützt aber jetzt .NET Standard 2.1 (.NET 5, 6), Unity und sogar TypeScript. Es unterstützt auch Polymorphismus (Union), Vollversionstoleranz, Zirkelverweise und die neuesten modernen I/O-APIs (IBufferWriter, ReadOnlySeqeunce, Pipelines).
Die Leistung des Serializers basiert sowohl auf der „Datenformatspezifikation“ als auch auf der „Implementierung in jeder Sprache“. Während binäre Formate beispielsweise im Allgemeinen einen Vorteil gegenüber Textformaten (wie JSON) haben, ist es möglich, einen JSON-Serializer zu haben, der schneller ist als ein binärer Serializer (wie mit Utf8Json demonstriert ). Was ist also der schnellste Serializer? Wenn Sie sich sowohl mit der Spezifikation als auch mit der Implementierung befassen, ist der wirklich schnellste Serializer geboren.
Ich entwickle und pflege MessagePack für C# seit vielen Jahren, und bin es immer noch, und MessagePack für C# ist ein sehr erfolgreicher Serialisierer in der .NET-Welt mit über 4000 GitHub-Sternen. Es wurde auch von Microsoft-Standardprodukten wie Visual Studio 2022, SignalR MessagePack Hub Protocol und dem Blazor Server-Protokoll (blazorpack) übernommen.
In den letzten 5 Jahren habe ich zudem fast 1000 Issues bearbeitet. Ich arbeite seit 5 Jahren an der AOT-Unterstützung mit Codegeneratoren, die Roslyn verwenden, und habe dies demonstriert, insbesondere in Unity, einer AOT-Umgebung (IL2CPP), und vielen Unity-Handyspielen, die sie verwenden.
Neben MessagePack für C# habe ich Serialisierer wie ZeroFormatter (eigenes Format) und Utf8Json (JSON) erstellt, die viele GitHub-Sterne erhalten haben, sodass ich ein tiefes Verständnis für die Leistungsmerkmale verschiedener Formate habe. Außerdem war ich an der Erstellung des RPC-Frameworks MagicOnion , der In-Memory-Datenbank MasterMemory , des PubSub-Clients AlterNats und sowohl Client- (Unity)/Server-Implementierungen mehrerer Spieletitel beteiligt.
Das Ziel von MemoryPack ist es, der ultimativ schnelle, praktische und vielseitige Serializer zu sein. Und ich glaube, ich habe es geschafft.
Inkrementeller Quellengenerator
MemoryPack übernimmt vollständig den in .NET 6 verbesserten Incremental Source Generator . In Bezug auf die Verwendung unterscheidet es sich nicht so sehr von MessagePack für C#, außer dass der Zieltyp in Partial geändert wird.
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);
Der Quellgenerator dient auch als Analysator, sodass er erkennen kann, ob er sicher serialisierbar ist, indem er zur Bearbeitungszeit einen Kompilierungsfehler ausgibt.
Beachten Sie, dass die Unity-Version aus Gründen der Sprach-/Compilerversion den alten Quellgenerator anstelle des inkrementellen Quellgenerators verwendet.
Binäre Spezifikation für C#
Der Slogan von MemoryPack lautet „Zero-Encoding“. Dies ist keine besondere Geschichte; Der wichtigste binäre Serialisierer von Rust, bincode , hat beispielsweise eine ähnliche Spezifikation. FlatBuffers liest und schreibt auch Inhalte ähnlich wie Speicherdaten ohne Parsing-Implementierung.
Im Gegensatz zu FlatBuffers und anderen ist MemoryPack jedoch ein universeller Serializer, der keinen speziellen Typ erfordert und gegen POCO serialisiert/deserialisiert. Es hat auch eine Versionsverwaltung, die tolerant gegenüber Schema-Member-Hinzufügungen und Polymorphismus-Unterstützung (Union) ist.
Varint-Codierung vs. behoben
Int32 besteht aus 4 Bytes, aber in JSON werden Zahlen beispielsweise als Zeichenfolgen mit variabler Längencodierung von 1 bis 11 Bytes codiert (z. B. 1 oder -2147483648). Viele Binärformate haben auch Codierungsspezifikationen mit variabler Länge von 1 bis 5 Bytes, um Größe zu sparen. Zum Beispiel hat der numerische Typ von Protokollpuffern eine ganzzahlige Codierung mit variabler Länge, die den Wert in 7 Bits und das Flag für das Vorhandensein oder Fehlen eines Folgenden in 1 Bit (Variante) speichert. Das bedeutet, je kleiner die Zahl, desto weniger Bytes werden benötigt. Umgekehrt wächst die Zahl im schlimmsten Fall auf 5 Bytes an, was größer ist als die ursprünglichen 4 Bytes. MessagePack und CBORwerden in ähnlicher Weise unter Verwendung einer Codierung mit variabler Länge verarbeitet, mit einem Minimum von 1 Byte für kleine Zahlen und einem Maximum von 5 Byte für große Zahlen.
Dies bedeutet, dass varint eine zusätzliche Verarbeitung als im Fall mit fester Länge ausgeführt wird. Vergleichen wir die beiden in konkretem Code. Variable Länge ist Varint + ZigZag-Codierung (negative und positive Zahlen werden kombiniert), die in protobuf verwendet wird.
// 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);
}
Dies ist sogar noch ausgeprägter, wenn es auf Arrays angewendet wird.
// https://sharplab.io/
Inspect.Heap(new int[]{ 1, 2, 3, 4, 5 });
// Fixed-length(MemoryPack)
void Serialize(int[] value)
{
// Size can be calculated and allocate in advance
var size = (sizeof(int) * value.Length) + 4;
EnsureCapacity(size);
// MemoryCopy once
MemoryMarshal.AsBytes(value.AsSpan()).CopyTo(buffer);
}
// Variable-length(MessagePack)合
void Serialize(int[] value)
{
foreach (var item in value)
{
// Unknown size, so check size each times
EnsureCapacity(); // if (buffer.Length < writeLength) Resize();
// Variable length encoding per element
WriteVarInt32(item);
}
}
Arrays in C# sind nicht nur primitive Typen wie int, das gilt auch für Structs mit mehreren Primitiven, beispielsweise hätte ein Vector3-Array mit (float x, float y, float z) das folgende Speicherlayout.
Ein Float (4 Bytes) hat in MessagePack eine feste Länge von 5 Bytes. Dem zusätzlichen 1 Byte ist eine Kennung vorangestellt, die angibt, um welchen Typ es sich bei dem Wert handelt (Int, Float, String…). Insbesondere [0xca, x, x, x, x, x]. Das MemoryPack-Format hat keine Kennung, daher werden 4 Bytes unverändert geschrieben.
Betrachten Sie Vector3[10000], der 50-mal besser war als der Benchmark.
// these fields exists in type
// byte[] buffer
// int offset
void SerializeMemoryPack(Vector3[] value)
{
// only do copy once
var 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 fields
foreach (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;
}
}
}
Mit MemoryPack nur eine einzige Speicherkopie. Dies würde die Verarbeitungszeit buchstäblich um eine Größenordnung ändern und ist der Grund für die 50- bis 100-fache Beschleunigung in der Grafik am Anfang dieses Artikels.
Natürlich ist der Deserialisierungsprozess auch eine einzelne Kopie.
// 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;
}
Die meisten Leute verwenden es jedoch wahrscheinlich nicht, und niemand würde eine proprietäre Option verwenden, die MessagePack inkompatibel machen würde.
Daher wollte ich mit MemoryPack eine Spezifikation, die standardmäßig die beste Leistung als C# liefert.
String-Optimierung
MemoryPack hat zwei Spezifikationen für String: UTF8 oder UTF16. Da die C#-Zeichenfolge UTF16 ist, spart die Serialisierung als UTF16 die Kosten für die Codierung/Decodierung in UTF8.
void EncodeUtf16(string value)
{
var size = value.Length * 2;
EnsureCapacity(size);
// Span<char> -> Span<byte> -> Copy
MemoryMarshal.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);
}
Aber selbst mit UTF8 hat MemoryPack einige Optimierungen, die andere Serialisierer nicht haben.
// 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);
}
Normalerweise dürfen Serialisierer einen großzügigen Puffer reservieren. Daher weist MemoryPack die dreifache Länge der Zeichenfolge zu, was der ungünstigste Fall für die UTF8-Codierung ist, um doppeltes Durchlaufen zu vermeiden.
Bei der Dekodierung werden weitere spezielle Optimierungen angewendet.
// 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));
}
var length = CalcUtf16Length(utf8data);
var str = String.Create(length);
Encoding.Utf8.DecodeToString(utf8data, str);
MemoryPack zeichnet jedoch sowohl UTF16-Length als auch UTF8-Length im Header auf. Daher bietet die Kombination aus „String.Create<TState>(Int32, TState, SpanAction<Char,TState>)“ und „Utf8.ToUtf16“ die effizienteste Decodierung für C#-String.
Über die Nutzlastgröße
Die Codierung von Ganzzahlen mit fester Länge kann im Vergleich zur Codierung mit variabler Länge in der Größe aufgeblasen werden. In der heutigen Zeit ist es jedoch eher ein Nachteil, die Codierung mit variabler Länge zu verwenden, nur um die geringe Größe von Ganzzahlen zu reduzieren.
Da es sich bei den Daten nicht nur um Ganzzahlen handelt, sollten Sie, wenn Sie die Größe wirklich reduzieren möchten, eine Komprimierung in Betracht ziehen ( LZ4 , ZStandard , Brotli usw.), und wenn Sie die Daten komprimieren, ist die Codierung mit variabler Länge fast sinnlos. Wenn Sie spezialisierter und kleiner sein möchten, erhalten Sie mit spaltenorientierter Komprimierung bessere Ergebnisse (z. B. Apache Parquet ).
Für eine effiziente Komprimierung, die in die MemoryPack-Implementierung integriert ist, habe ich derzeit standardmäßig Hilfsklassen für BrotliEncode/Decode.
Ich habe auch mehrere Attribute, die eine spezielle Komprimierung auf bestimmte primitive Spalten anwenden, wie z. B. die Spaltenkomprimierung.
[MemoryPackable]
public partial class Sample
{
public int Id { get; set; }
[BitPackFormatter]
public bool[] Data { get; set; }
[BrotliFormatter]
public byte[] Payload { get; set; }
}
BrotliFormatter wendet den Komprimierungsalgorithmus direkt an. Dies ist tatsächlich besser als das Komprimieren der gesamten Datei.
Dies liegt daran, dass keine Zwischenkopie benötigt wird und der Komprimierungsprozess direkt auf die serialisierten Daten angewendet werden kann.
Die Methode zum Extrahieren von Leistung und Komprimierungsverhältnis durch Anwenden einer benutzerdefinierten Verarbeitung in Abhängigkeit von den Daten anstelle einer einfachen Gesamtkomprimierung wird im Artikel Reducing Logging Cost by Two Orders of Magnitude using CLP im Uber Engineering Blog beschrieben.
Neue Funktionen von .NET 7 / C#11 verwenden
MemoryPack hat leicht unterschiedliche Methodensignaturen in der Implementierung für .NET Standard 2.1 und der Implementierung für .NET 7. .NET 7 ist eine aggressivere, leistungsorientierte Implementierung, die die neuesten Sprachfeatures nutzt.
Erstens verwendet die Serializer-Schnittstelle statische abstrakte Mitglieder wie folgt
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/6010
static 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);
}
[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 directly
T.Serialize(ref this, ref Unsafe.AsRef(value));
}
//
public void WriteValue<T>(scoped in T? value)
{
// call Serialize from interface virtual method
IMemoryPackFormatter<T> formatter = MemoryPackFormatterProvider.GetFormatter<T>();
formatter.Serialize(ref this, ref Unsafe.AsRef(value));
}
public ref struct MemoryPackWriter<TBufferWriter>
where TBufferWriter : IBufferWriter<byte>
{
ref TBufferWriter bufferWriter;
ref byte bufferReference;
int bufferLength;
// internally MemoryPack uses some struct buffer-writers
struct BrotliCompressor : IBufferWriter<byte>
struct FixedArrayBufferWriter : IBufferWriter<byte>
Beispielsweise können Sammlungen als IEnumerable<T> für eine allgemeine Implementierung serialisiert/deserialisiert werden, aber MemoryPack bietet eine separate Implementierung für alle Typen. Der Einfachheit halber kann eine List<T> verarbeitet werden als
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);
}
}
MemoryPack hat es jedoch weiter optimiert.
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);
}
Auch bei Deserialize gibt es einige interessante Optimierungen. Erstens akzeptiert Deserialize von MemoryPack ein ref T? value, und wenn der Wert null ist, wird das intern generierte Objekt (genau wie ein normaler Serializer) überschrieben, wenn der Wert übergeben wird. Dies ermöglicht eine Nullzuordnung der Erstellung neuer Objekte während der Deserialisierung. Sammlungen werden auch wiederverwendet, indem Clear() im Fall von List<T> aufgerufen wird.
Durch einen speziellen Span-Aufruf wird dann alles als Span behandelt, wodurch der zusätzliche Overhead von List<T>.Add vermieden wird.
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]);
}
}
}
Aber CollectionsMarshal.AsSpan, Sie erhalten eine Spanne der Länge 0, da die interne Größe nicht geändert wird. Wenn wir CollectionMarshals.AsMemory hätten, könnten wir das rohe Array von dort mit einer MemoryMarshal.TryGetArray-Kombination abrufen, aber leider gibt es keine Möglichkeit, das ursprüngliche Array aus Span zu erhalten. Also erzwinge ich, dass die Typstruktur mit Unsafe.As übereinstimmt, und ändere List<T>._size, ich konnte das erweiterte interne Array abrufen.
Auf diese Weise könnten wir den nicht verwalteten Typ optimieren, um ihn einfach zu kopieren, und List<T>.Add (das jedes Mal die Arraygröße überprüft) vermeiden und die Werte über Span<T>[index] packen, was viel höher ist als die Deserialisierung eines herkömmlichen Serializers. Leistung.
Während die Optimierung für List<T> repräsentativ ist, gibt es zu viele andere, um sie einzuführen, alle Typen wurden untersucht und die bestmögliche Optimierung wurde auf jeden angewendet.
Serialize akzeptiert IBufferWriter<byte> als native Struktur und Deserialize akzeptiert ReadOnlySpan<byte> und ReadOnlySequence<byte>.
Dies liegt daran, dass diese Typen von System.IO.Pipelines benötigt werden . Mit anderen Worten, da es die Grundlage des Servers von ASP .NET Core (Kestrel) ist, können Sie eine Serialisierung mit höherer Leistung erwarten, indem Sie eine direkte Verbindung damit herstellen.
IBufferWriter<byte> ist besonders wichtig, da es direkt in den Puffer schreiben kann, wodurch im Serialisierungsprozess eine Nullkopie erreicht wird. Die Unterstützung für IBufferWriter<byte> ist eine Voraussetzung für moderne Serialisierer, da sie eine höhere Leistung bietet als die Verwendung von byte[] oder Stream. Der Serializer für das Diagramm am Anfang (System.Text.Json, protobuf-net, Microsoft.Orleans.Serialization, MessagePack for C# und MemoryPack) unterstützt dies.
MessagePack vs. MemoryPack
MessagePack für C# ist sehr einfach zu verwenden und hat eine hervorragende Leistung. Insbesondere die folgenden Punkte sind besser als MemoryPack
- Hervorragende Sprachkompatibilität
- JSON-Kompatibilität (insbesondere für Zeichenfolgenschlüssel) und menschliche Lesbarkeit
- Perfekte Versionstoleranz standardmäßig
- Serialisierung von Objekt- und anonymen Typen
- dynamische Deserialisierung
- eingebettete LZ4-Komprimierung
- Langjährig bewährte Stabilität
Es ist MessagePack jedoch in folgenden Punkten überlegen
- Leistung, insbesondere für nicht verwaltete Arrays
- Einfach zu bedienende AOT-Unterstützung
- Extended Polymorphism (Union) Konstruktionsmethode
- Unterstützung für Zirkelverweise
- Deserialisierung überschreiben
- TypeScript-Code-Generierung
- Flexibler attributbasierter benutzerdefinierter Formatierer
MemoryPack ist kein experimenteller Serializer, der sich nur auf die Leistung konzentriert, sondern soll auch ein praktischer Serializer sein. Zu diesem Zweck habe ich auch auf meiner Erfahrung mit MessagePack für C# aufgebaut, um eine Reihe von Funktionen bereitzustellen.
- Unterstützung moderner E/A-APIs (`IBufferWriter<byte>`, `ReadOnlySpan<byte>`, `ReadOnlySequence<byte>`)
- Native AOT-freundliche Quellgenerator-basierte Codegenerierung, kein dynamischer CodeGen (IL.Emit)
- Reflexionsfreie, nicht generische APIs
- In vorhandene Instanz deserialisieren
- Serialisierung von Polymorphismus (Union).
- eingeschränkte Versionstoleranz (schnell/Standard) und volle Versionstoleranz
- Zirkuläre Referenzserialisierung
- PipeWriter/Reader-basierte Streaming-Serialisierung
- TypeScript-Codegenerierung und ASP.NET Core Formatter
- Unity (2021.3) IL2CPP-Unterstützung über .NET Source Generator

![Was ist überhaupt eine verknüpfte Liste? [Teil 1]](https://post.nghiatu.com/assets/images/m/max/724/1*Xokk6XOjWyIGCBujkJsCzQ.jpeg)



































