Jak zrobić najszybszy serializator .NET z .NET 7 / C# 11, w przypadku MemoryPack
Wydałem nowy serializator o nazwie MemoryPack , nowy serializator specyficzny dla języka C#, który działa znacznie szybciej niż inne serializatory.

W porównaniu z MessagePack dla C# , szybkim serializatorem binarnym, wydajność jest kilka razy większa dla standardowych obiektów, a nawet 50~100 razy większa, gdy dane są optymalne. Najlepszą obsługą jest .NET 7, ale teraz obsługuje .NET Standard 2.1 (.NET 5, 6), Unity, a nawet TypeScript. Obsługuje również polimorfizm (Union), pełną tolerancję wersji, odwołania cykliczne oraz najnowsze nowoczesne interfejsy API wejścia/wyjścia (IBufferWriter, ReadOnlySeqeunce, Pipelines).
Wydajność serializatora opiera się zarówno na „specyfikacji formatu danych”, jak i „implementacji w każdym języku”. Na przykład, podczas gdy formaty binarne mają na ogół przewagę nad formatami tekstowymi (takimi jak JSON), możliwe jest posiadanie serializatora JSON, który jest szybszy niż serializator binarny (jak pokazano w przypadku Utf8Json ). Więc jaki jest najszybszy serializator? Kiedy przejdziesz do specyfikacji i implementacji, narodzi się prawdziwy najszybszy serializator.
Od wielu lat rozwijam i utrzymuję MessagePack dla C#, a MessagePack dla C# jest bardzo udanym serializatorem w świecie .NET, z ponad 4000 GitHub Stars. Został on również przyjęty przez standardowe produkty firmy Microsoft, takie jak Visual Studio 2022, SignalR MessagePack Hub Protocol i protokół Blazor Server (blazorpack).
W ciągu ostatnich 5 lat przetworzyłem również blisko 1000 spraw. Pracowałem nad obsługą AOT z generatorami kodu używającymi Roslyn od 5 lat temu i zademonstrowałem to, szczególnie w Unity, środowisku AOT (IL2CPP) i wielu grach mobilnych Unity, które go używają.
Oprócz MessagePack dla C# stworzyłem serializatory, takie jak ZeroFormatter (własny format) i Utf8Json (JSON), które otrzymały wiele GitHub Stars, więc mam dogłębne zrozumienie charakterystyki wydajnościowej różnych formatów. Brałem również udział w tworzeniu frameworka RPC MagicOnion , in-memory bazy danych MasterMemory , klienta PubSub AlterNats oraz implementacji klient (Unity)/serwer kilku tytułów gier.
Celem MemoryPack jest bycie najlepszym szybkim, praktycznym i wszechstronnym serializatorem. I myślę, że to osiągnąłem.
Przyrostowy generator źródła
MemoryPack w pełni przyjmuje przyrostowy generator źródła udoskonalony w .NET 6. Pod względem użytkowania nie różni się tak bardzo od MessagePack dla C#, z wyjątkiem zmiany typu docelowego na częściowy.
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);

Generator źródła służy również jako analizator, dzięki czemu może wykryć, czy można go bezpiecznie serializować, wysyłając błąd kompilacji w czasie edycji.
Należy zauważyć, że wersja Unity używa starego generatora źródła zamiast przyrostowego generatora źródła z powodu wersji językowej/kompilatora.
Binarna specyfikacja dla C#
Sloganem MemoryPack jest „Zero kodowania”. To nie jest szczególna historia; Na przykład główny serializator binarny Rusta, bincode , ma podobną specyfikację. FlatBuffers również odczytuje i zapisuje zawartość podobną do danych pamięci bez implementacji parsowania.
Jednak w przeciwieństwie do FlatBuffers i innych, MemoryPack jest serializatorem ogólnego przeznaczenia, który nie wymaga specjalnego typu i serializuje/deserializuje przeciwko POCO. Ma również tolerancję wersji dla dodawania elementów członkowskich schematu i obsługę polimorfizmu (Union).
kodowanie varint vs stałe
Int32 ma 4 bajty, ale na przykład w JSON liczby są kodowane jako łańcuchy o zmiennej długości kodowania 1 ~ 11 bajtów (np. 1 lub -2147483648). Wiele formatów binarnych ma również specyfikacje kodowania o zmiennej długości od 1 do 5 bajtów, aby zaoszczędzić rozmiar. Na przykład numeryczny typ buforów protokołów ma kodowanie liczb całkowitych o zmiennej długości, które przechowuje wartość w 7 bitach, a flagę obecności lub braku następującej wartości w 1 bicie (varint). Oznacza to, że im mniejsza liczba, tym mniej bajtów jest wymaganych. I odwrotnie, w najgorszym przypadku liczba wzrośnie do 5 bajtów, czyli więcej niż oryginalne 4 bajty. MessagePack i CBORsą podobnie przetwarzane przy użyciu kodowania o zmiennej długości, z minimum 1 bajtem dla małych liczb i maksymalnie 5 bajtami dla dużych liczb.
Oznacza to, że varint jest przetwarzany dodatkowo niż w przypadku o stałej długości. Porównajmy te dwa w konkretnym kodzie. Zmienna długość to kodowanie varint + ZigZag (liczby ujemne i dodatnie są łączone) używane w 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);
}
Jest to jeszcze bardziej widoczne, gdy zastosuje się je do tablic.
// 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);
}
}
Tablice w C# to nie tylko prymitywne typy, takie jak int, dotyczy to również struktur z wieloma prymitywami, na przykład tablica Vector3 z (float x, float y, float z) miałaby następujący układ pamięci.

Liczba zmiennoprzecinkowa (4 bajty) to stała długość 5 bajtów w MessagePack. Dodatkowy 1 bajt jest poprzedzony identyfikatorem wskazującym typ wartości (Int, Float, String…). W szczególności [0xca, x, x, x, x, x]. Format MemoryPack nie ma identyfikatora, więc 4 bajty są zapisywane bez zmian.
Weźmy pod uwagę Vector3[10000], który był 50 razy lepszy niż test porównawczy.
// 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;
}
}
}
Z MemoryPack tylko pojedyncza kopia pamięci. To dosłownie zmieniłoby czas przetwarzania o rząd wielkości i jest przyczyną przyspieszenia 50x~100x na wykresie na początku tego artykułu.
Oczywiście proces deserializacji to także pojedyncza kopia.
// 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;
}
Jednak większość ludzi prawdopodobnie go nie używa i nikt nie użyłby zastrzeżonej opcji, która sprawiłaby, że MessagePack byłby niekompatybilny.
Tak więc w przypadku MemoryPack chciałem specyfikacji, która domyślnie dawałaby najlepszą wydajność jako C#.
Optymalizacja ciągów
MemoryPack ma dwie specyfikacje dla String: UTF8 lub UTF16. ponieważ łańcuch C # to UTF16, serializacja go jako UTF16 oszczędza koszty kodowania/dekodowania do 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);
}
Jednak nawet w przypadku UTF8 MemoryPack ma pewne optymalizacje, których nie mają inne serializatory.
// 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);
}
Zazwyczaj serializatory mogą rezerwować obfity bufor. Dlatego MemoryPack przydziela trzykrotność długości łańcucha, co jest najgorszym przypadkiem w przypadku kodowania UTF8, aby uniknąć podwójnego przejścia.
W przypadku dekodowania stosowane są dalsze specjalne optymalizacje.
// 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);
Jednak MemoryPack rejestruje zarówno długość UTF16, jak i długość UTF8 w nagłówku. W związku z tym kombinacja String.Create<TState>(Int32, TState, SpanAction<Char,TState>) i Utf8.ToUtf16 zapewnia najbardziej wydajne dekodowanie C# String.
O wielkości ładunku
Kodowanie liczb całkowitych o stałej długości może mieć zawyżony rozmiar w porównaniu z kodowaniem o zmiennej długości. Jednak w epoce nowożytnej używanie kodowania o zmiennej długości tylko w celu zmniejszenia małego rozmiaru liczb całkowitych jest bardziej niekorzystne.
Ponieważ dane to nie tylko liczby całkowite, jeśli naprawdę chcesz zmniejszyć rozmiar, powinieneś rozważyć kompresję ( LZ4 , ZStandard , Brotli itp.), A jeśli skompresujesz dane, nie ma prawie sensu kodowanie o zmiennej długości. Jeśli chcesz być bardziej wyspecjalizowany i mniejszy, kompresja zorientowana na kolumny da lepsze wyniki (np. Apache Parquet ).
Dla wydajnej kompresji zintegrowanej z implementacją MemoryPack mam obecnie w standardzie klasy pomocnicze dla BrotliEncode/Decode.
Mam również kilka atrybutów, które stosują specjalną kompresję do niektórych prymitywnych kolumn, takich jak kompresja kolumn.
[MemoryPackable]
public partial class Sample
{
public int Id { get; set; }
[BitPackFormatter]
public bool[] Data { get; set; }
[BrotliFormatter]
public byte[] Payload { get; set; }
}
BrotliFormatter bezpośrednio stosuje algorytm kompresji. W rzeczywistości działa to lepiej niż kompresja całego pliku.

Dzieje się tak, ponieważ nie jest potrzebna żadna kopia pośrednia, a proces kompresji można zastosować bezpośrednio do serializowanych danych.
Metoda wyodrębniania wydajności i współczynnika kompresji poprzez zastosowanie przetwarzania w niestandardowy sposób w zależności od danych zamiast zwykłej kompresji ogólnej została szczegółowo opisana w artykule Redukcja kosztów rejestrowania o dwa rzędy wielkości przy użyciu CLP na blogu Uber Engineering Blog.
Korzystanie z nowych funkcji platformy .NET 7 / C#11
MemoryPack ma nieco inne sygnatury metod w implementacji dla .NET Standard 2.1 i implementacji dla .NET 7. .NET 7 to bardziej agresywna, zorientowana na wydajność implementacja, która wykorzystuje najnowsze funkcje języka.
Po pierwsze, interfejs serializatora wykorzystuje statyczne składowe abstrakcyjne w następujący sposób
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>
Na przykład kolekcje mogą być serializowane/deserializowane jako IEnumerable<T> dla wspólnej implementacji, ale MemoryPack zapewnia oddzielną implementację dla wszystkich typów. Dla uproszczenia List<T> można przetwarzać jako
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);
}
}
Jednak MemoryPack dodatkowo go zoptymalizował.
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);
}
W przypadku deserializacji istnieje również kilka interesujących optymalizacji. Po pierwsze Deserialize w MemoryPack akceptuje ref T? value, a jeśli wartość jest pusta, nadpisze wewnętrznie wygenerowany obiekt (tak jak normalny serializator), jeśli wartość zostanie przekazana. Pozwala to na zerową alokację tworzenia nowych obiektów podczas deserializacji. Kolekcje są również ponownie wykorzystywane przez wywołanie Clear() w przypadku List<T>.
Następnie, wykonując specjalne wywołanie Span, wszystko jest obsługiwane jako Spans, unikając dodatkowego narzutu 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]);
}
}
}
Ale CollectionsMarshal.AsSpan, otrzymasz rozpiętość o długości 0, ponieważ rozmiar wewnętrzny nie zostanie zmieniony. Gdybyśmy mieli CollectionMarshals.AsMemory, moglibyśmy pobrać stamtąd surową tablicę za pomocą kombinacji MemoryMarshal.TryGetArray, ale niestety nie ma możliwości uzyskania oryginalnej tablicy ze Span. Wymuszam więc zgodność struktury typu z Unsafe.As i zmieniam List<T>._size, udało mi się uzyskać rozszerzoną tablicę wewnętrzną.
W ten sposób moglibyśmy zoptymalizować typ niezarządzany, aby po prostu go skopiować i uniknąć List<T>.Add (który za każdym razem sprawdza rozmiar tablicy) i spakować wartości za pomocą Span<T>[index], który jest znacznie wyższy niż deserializacji konwencjonalnego serializatora. wydajność.
Chociaż optymalizacja List<T> jest reprezentatywna, istnieje zbyt wiele innych do wprowadzenia, wszystkie typy zostały zbadane i do każdego z nich zastosowano najlepszą możliwą optymalizację.
Serialize akceptuje IBufferWriter<byte> jako swoją natywną strukturę, a Deserialize akceptuje ReadOnlySpan<byte> i ReadOnlySequence<byte>.
Dzieje się tak, ponieważ te typy są wymagane przez System.IO.Pipelines . Innymi słowy, ponieważ jest to podstawa serwera ASP .NET Core (Kestrel), można oczekiwać serializacji o wyższej wydajności, łącząc się bezpośrednio z nim.
IBufferWriter<byte> jest szczególnie ważny, ponieważ może zapisywać bezpośrednio do bufora, uzyskując w ten sposób zero kopii w procesie serializacji. Obsługa IBufferWriter<byte> jest warunkiem wstępnym dla nowoczesnych serializatorów, ponieważ oferuje wyższą wydajność niż przy użyciu byte[] lub Stream. Serializator dla wykresu na początku (System.Text.Json, protobuf-net, Microsoft.Orleans.Serialization, MessagePack dla języka C# i MemoryPack) obsługuje go.
MessagePack kontra MemoryPack
MessagePack dla C# jest bardzo łatwy w użyciu i ma doskonałą wydajność. W szczególności następujące punkty są lepsze niż MemoryPack
- Doskonała kompatybilność między językami
- Kompatybilność JSON (szczególnie w przypadku kluczy łańcuchowych) i czytelność dla człowieka
- Domyślnie toleruje idealną wersję
- Serializacja typów obiektowych i anonimowych
- dynamiczna deserializacja
- osadzona kompresja LZ4
- Długo sprawdzona stabilność
Jest jednak lepszy od MessagePack pod następującymi względami
- Wydajność, szczególnie w przypadku tablic typu niezarządzanego
- Łatwa w użyciu obsługa AOT
- Metoda konstrukcji rozszerzonego polimorfizmu (unia).
- Obsługa odwołań cyklicznych
- Zastąp deserializację
- Generowanie kodu TypeScript
- Elastyczny formater niestandardowy oparty na atrybutach
MemoryPack nie jest eksperymentalnym serializatorem, który koncentruje się tylko na wydajności, ale ma być również praktycznym serializatorem. W tym celu oparłem się również na moim doświadczeniu z MessagePack dla C#, aby zapewnić szereg funkcji.
- Obsługa nowoczesnych interfejsów API wejścia/wyjścia (`IBufferWriter<byte>`, `ReadOnlySpan<byte>`, `ReadOnlySequence<byte>`)
- Natywne, przyjazne AOT generowanie kodu oparte na generatorze źródeł, bez Dynamic CodeGen (IL.Emit)
- Bezrefleksyjne, nieogólne interfejsy API
- Deserializuj do istniejącej instancji
- Serializacja polimorfizmu (unia).
- ograniczona tolerancja wersji (szybka/domyślna) i pełna tolerancja wersji
- Cykliczna serializacja referencyjna
- Serializacja strumieniowa oparta na PipeWriter/Reader
- Generowanie kodu TypeScript i ASP.NET Core Formatter
- Unity(2021.3) Obsługa IL2CPP za pośrednictwem generatora źródeł .NET