Cara membuat .NET Serializer tercepat dengan .NET 7 / C# 11, kasus MemoryPack

Nov 29 2022
Saya telah merilis serializer baru bernama MemoryPack, serializer khusus C# baru yang bekerja jauh lebih cepat daripada serializer lainnya. Dibandingkan dengan MessagePack untuk C#, serializer biner cepat, kinerjanya beberapa kali lebih cepat untuk objek standar, dan bahkan 50~100 kali lebih cepat saat data optimal.

Saya telah merilis serializer baru bernama MemoryPack , serializer khusus C# baru yang bekerja jauh lebih cepat daripada serializer lainnya.

Dibandingkan dengan MessagePack untuk C# , serializer biner cepat, kinerjanya beberapa kali lebih cepat untuk objek standar, dan bahkan 50~100 kali lebih cepat saat data optimal. Dukungan terbaik adalah .NET 7, tetapi sekarang mendukung .NET Standard 2.1 (.NET 5, 6), Unity, dan bahkan TypeScript. Ini juga mendukung Polymorphism(Union), full version-toleran, referensi melingkar, dan API I/O modern terbaru (IBufferWriter, ReadOnlySeqeunce, Pipelines).

Performa serializer didasarkan pada "spesifikasi format data" dan "implementasi dalam setiap bahasa". Misalnya, meskipun format biner umumnya memiliki keunggulan dibandingkan format teks (seperti JSON), dimungkinkan untuk memiliki serializer JSON yang lebih cepat daripada serializer biner (seperti yang ditunjukkan dengan Utf8Json ). Jadi apa serializer tercepat? Ketika Anda turun ke spesifikasi dan implementasi, serializer tercepat yang sebenarnya lahir.

Saya telah, dan masih, mengembangkan dan memelihara MessagePack untuk C# selama bertahun-tahun, dan MessagePack untuk C# adalah serializer yang sangat sukses di dunia .NET, dengan lebih dari 4000 Bintang GitHub. Itu juga telah diadopsi oleh produk standar Microsoft seperti Visual Studio 2022, SignalR MessagePack Hub Protocol , dan protokol Blazor Server (blazorpack).

Dalam 5 tahun terakhir, saya juga telah memproses hampir 1000 masalah. Saya telah mengerjakan dukungan AOT dengan pembuat kode menggunakan Roslyn sejak 5 tahun lalu, dan telah mendemonstrasikannya, terutama di Unity, lingkungan AOT (IL2CPP), dan banyak game seluler Unity yang menggunakannya.

Selain MessagePack untuk C#, saya telah membuat serializer seperti ZeroFormatter (format sendiri) dan Utf8Json (JSON), yang telah menerima banyak Bintang GitHub, jadi saya memiliki pemahaman yang mendalam tentang karakteristik kinerja berbagai format. Selain itu, saya telah terlibat dalam pembuatan kerangka kerja RPC MagicOnion , basis data dalam memori MasterMemory , klien PubSub AlterNats , dan implementasi klien (Unity)/server dari beberapa judul game.

Tujuan MemoryPack adalah menjadi serializer yang paling cepat, praktis, dan serbaguna. Dan saya pikir saya mencapainya.

Generator Sumber Tambahan

MemoryPack sepenuhnya mengadopsi Incremental Source Generator yang disempurnakan di .NET 6. Dalam hal penggunaan, tidak jauh berbeda dengan MessagePack untuk C#, kecuali untuk mengubah tipe target menjadi parsial.

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 Sumber juga berfungsi sebagai penganalisa, sehingga dapat mendeteksi apakah dapat diserialisasi dengan aman dengan mengeluarkan kesalahan kompilasi pada waktu edit.

Perhatikan bahwa versi Unity menggunakan Generator Sumber lama alih-alih Generator Sumber Inkremental karena alasan versi bahasa/kompiler.

Spesifikasi Biner untuk C#

Slogan MemoryPack adalah "Nol encoding". Ini bukan cerita khusus; Serializer biner utama Rust, bincode , misalnya, memiliki spesifikasi serupa. FlatBuffers juga membaca dan menulis konten yang mirip dengan data memori tanpa implementasi parsing.

Namun, tidak seperti FlatBuffers dan lainnya, MemoryPack adalah serializer serba guna yang tidak memerlukan tipe khusus dan membuat serial/deserialisasi terhadap POCO. Ini juga memiliki versi yang toleran terhadap penambahan anggota skema dan dukungan polimorfisme (Union).

pengkodean varian vs diperbaiki

Int32 berukuran 4 byte, tetapi dalam JSON, misalnya, angka dikodekan sebagai string dengan panjang variabel pengkodean 1~11 byte (misalnya, 1 atau -2147483648). Banyak format biner juga memiliki spesifikasi pengkodean panjang variabel 1 hingga 5 byte untuk menghemat ukuran. Misalnya, tipe numerik dari Protocol Buffer memiliki pengkodean integer dengan panjang variabel yang menyimpan nilai dalam 7 bit dan tanda untuk ada atau tidaknya berikut dalam 1 bit (varint). Ini berarti semakin kecil angkanya, semakin sedikit byte yang dibutuhkan. Sebaliknya, dalam kasus terburuk, jumlahnya akan bertambah menjadi 5 byte, yang lebih besar dari aslinya 4 byte. MessagePack dan CBORdiproses serupa menggunakan pengkodean panjang variabel, dengan minimum 1 byte untuk bilangan kecil dan maksimum 5 byte untuk bilangan besar.

Ini berarti varint menjalankan pemrosesan ekstra daripada dalam case dengan panjang tetap. Mari kita bandingkan keduanya dalam kode konkret. Panjang variabel adalah pengkodean varint + ZigZag (bilangan negatif dan positif digabungkan) yang digunakan dalam 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);
}

Ini bahkan lebih jelas bila diterapkan pada array.

// 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);
    }
}

Array di C# bukan hanya tipe primitif seperti int, ini juga berlaku untuk struct dengan banyak primitif, misalnya, array Vector3 dengan (float x, float y, float z) akan memiliki tata letak memori berikut.

Pelampung (4 byte) adalah panjang tetap 5 byte di MessagePack. Tambahan 1 byte diawali dengan pengenal yang menunjukkan jenis nilainya (Int, Float, String…). Khususnya, [0xca, x, x, x, x, x]. Format MemoryPack tidak memiliki pengidentifikasi, jadi 4 byte ditulis apa adanya.

Pertimbangkan Vector3[10000], yang 50 kali lebih baik dari tolok ukur.

// 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;
        }
    }
}

Dengan MemoryPack, hanya satu salinan memori. Ini benar-benar akan mengubah waktu pemrosesan dengan urutan besarnya dan merupakan alasan percepatan 50x~100x dalam grafik di awal artikel ini.

Tentu saja, proses deserialisasi juga merupakan satu salinan.

// 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;
}

Namun, kebanyakan orang mungkin tidak menggunakannya, dan tidak ada yang akan menggunakan opsi eksklusif yang akan membuat MessagePack tidak kompatibel.

Jadi dengan MemoryPack, saya menginginkan spesifikasi yang memberikan kinerja terbaik sebagai C# secara default.

Optimasi String

MemoryPack memiliki dua spesifikasi untuk String: UTF8 atau UTF16. karena string C# adalah UTF16, membuat serial sebagai UTF16 menghemat biaya penyandian/dekode ke 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);
}

Namun, bahkan dengan UTF8, MemoryPack memiliki beberapa pengoptimalan yang tidak dimiliki serializer lain.

// 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);
}

Biasanya, pembuat serial diizinkan untuk memesan buffer yang banyak. Oleh karena itu, MemoryPack mengalokasikan tiga kali panjang string, yang merupakan kasus terburuk untuk pengkodean UTF8, untuk menghindari traversal ganda.

Dalam kasus decoding, pengoptimalan khusus lebih lanjut diterapkan.

// 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);

Namun, MemoryPack mencatat Panjang UTF16 dan Panjang UTF8 di header. Oleh karena itu, kombinasi dari String.Create<TState>(Int32, TState, SpanAction<Char,TState>) dan Utf8.ToUtf16 menyediakan decoding paling efisien untuk C# String.

Tentang ukuran muatan

Pengkodean bilangan bulat dengan panjang tetap dapat digelembungkan ukurannya dibandingkan dengan pengkodean dengan panjang variabel. Namun, di era modern, menggunakan pengkodean panjang variabel hanya untuk mengurangi ukuran bilangan bulat yang kecil lebih merupakan kerugian.

Karena datanya bukan hanya bilangan bulat, jika Anda benar-benar ingin mengurangi ukurannya, Anda harus mempertimbangkan kompresi ( LZ4 , ZStandard , Brotli , dll.), dan jika Anda mengompresi data, hampir tidak ada gunanya pengodean panjang variabel. Jika Anda ingin lebih terspesialisasi dan lebih kecil, kompresi berorientasi kolom akan memberi Anda hasil yang lebih besar (misalnya, Apache Parquet ).

Untuk kompresi efisien yang terintegrasi dengan implementasi MemoryPack, saat ini saya memiliki kelas tambahan untuk BrotliEncode/Decode sebagai standar.

Saya juga memiliki beberapa atribut yang menerapkan kompresi khusus ke kolom primitif tertentu, seperti kompresi kolom.

[MemoryPackable]
public partial class Sample
{
    public int Id { get; set; }

    [BitPackFormatter]
    public bool[] Data { get; set; }

    [BrotliFormatter]
    public byte[] Payload { get; set; }
}

BrotliFormatter langsung menerapkan algoritma kompresi. Ini sebenarnya berkinerja lebih baik daripada mengompresi seluruh file.

Ini karena tidak diperlukan salinan perantara dan proses kompresi dapat diterapkan langsung ke data serial.

Metode penggalian kinerja dan rasio kompresi dengan menerapkan pemrosesan dengan cara khusus tergantung pada data, daripada kompresi keseluruhan sederhana, dirinci dalam Mengurangi Biaya Logging dengan Dua Urutan Magnitudo menggunakan artikel CLP di Blog Teknik Uber.

Menggunakan fitur baru .NET 7 / C#11

MemoryPack memiliki tanda tangan metode yang sedikit berbeda dalam implementasi untuk .NET Standard 2.1 dan implementasi untuk .NET 7. .NET 7 adalah implementasi berorientasi kinerja yang lebih agresif yang memanfaatkan fitur bahasa terbaru.

Pertama, antarmuka serializer menggunakan anggota abstrak statis sebagai berikut

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>

Misalnya, koleksi dapat diserialkan/dideserialisasi sebagai IEnumerable<T> untuk implementasi umum, tetapi MemoryPack menyediakan implementasi terpisah untuk semua jenis. Untuk kesederhanaan, List<T> dapat diproses sebagai

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);
    }
}

Namun, MemoryPack telah mengoptimalkannya lebih lanjut.

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);
}

Dalam kasus Deserialize, ada beberapa pengoptimalan yang menarik juga. Pertama, MemoryPack's Deserialize menerima referensi T? nilai, dan jika nilainya nol, itu akan menimpa objek yang dihasilkan secara internal (seperti serializer normal), jika nilainya diteruskan. Ini memungkinkan alokasi nol untuk pembuatan objek baru selama Deserialisasi. Koleksi juga digunakan kembali dengan memanggil Clear() dalam kasus List<T>.

Kemudian, dengan membuat panggilan Span khusus, semuanya ditangani sebagai Span, menghindari overhead tambahan dari 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]);
        }
    }
}

Tapi CollectionsMarshal.AsSpan, Anda akan mendapatkan rentang panjang 0, karena ukuran internal tidak berubah. Jika kita memiliki CollectionMarshals.AsMemory, kita bisa mendapatkan array mentah dari sana dengan kombo MemoryMarshal.TryGetArray, tetapi sayangnya tidak ada cara untuk mendapatkan array asli dari Span. Jadi, saya memaksa struktur tipe agar cocok dengan Unsafe.As dan mengubah List<T>._size, saya bisa mendapatkan array internal yang diperluas.

Dengan begitu, kita dapat mengoptimalkan tipe unmanaged untuk hanya menyalinnya, dan menghindari List<T>.Add (yang memeriksa ukuran array setiap kali), dan mengemas nilai melalui Span<T>[indeks], yang jauh lebih tinggi daripada deserialization dari serializer konvensional. pertunjukan.

Meskipun pengoptimalan untuk List<T> representatif, ada terlalu banyak yang lain untuk diperkenalkan, semua jenis telah diteliti dan kemungkinan pengoptimalan terbaik telah diterapkan untuk masing-masing.

Serialize menerima IBufferWriter<byte> sebagai struktur aslinya dan Deserialize menerima ReadOnlySpan<byte> dan ReadOnlySequence<byte>.

Ini karena jenis ini diperlukan oleh System.IO.Pipelines . Dengan kata lain, karena ini adalah dasar dari Server ASP .NET Core (Kestrel), Anda dapat mengharapkan serialisasi kinerja yang lebih tinggi dengan menyambungkannya langsung.

IBufferWriter<byte> sangat penting karena dapat menulis langsung ke buffer, sehingga mencapai salinan nol dalam proses serialisasi. Dukungan untuk IBufferWriter<byte> adalah prasyarat untuk serializer modern, karena menawarkan kinerja yang lebih tinggi daripada menggunakan byte[] atau Stream. Serializer untuk grafik di awal (System.Text.Json, protobuf-net, Microsoft.Orleans.Serialization, MessagePack for C#, dan MemoryPack) mendukungnya.

MessagePack vs MemoryPack

MessagePack untuk C# sangat mudah digunakan dan memiliki kinerja yang sangat baik. Secara khusus, poin-poin berikut lebih baik daripada MemoryPack

  • Kompatibilitas antar bahasa yang sangat baik
  • Kompatibilitas JSON (terutama untuk kunci string) dan keterbacaan manusia
  • Toleransi versi sempurna secara default
  • Serialisasi objek dan jenis anonim
  • deserialisasi dinamis
  • kompresi LZ4 tertanam
  • Stabilitas yang telah lama terbukti

Namun, ini lebih unggul dari MessagePack dalam hal berikut

  • Performa, terutama untuk tipe array yang tidak dikelola
  • Dukungan AOT yang mudah digunakan
  • Metode konstruksi Extended Polymorphism (Union).
  • Dukungan untuk referensi melingkar
  • Timpa deserialisasi
  • Pembuatan kode TypeScript
  • Pemformat khusus berbasis atribut yang fleksibel

MemoryPack bukanlah serializer eksperimental yang hanya berfokus pada kinerja, tetapi juga dimaksudkan untuk menjadi serializer yang praktis. Untuk tujuan ini, saya juga membangun pengalaman saya dengan MessagePack untuk C# untuk menyediakan sejumlah fitur.

  • Mendukung API I/O modern(`IBufferWriter<byte>`, `ReadOnlySpan<byte>`, `ReadOnlySequence<byte>`)
  • Native AOT friendly Source Generator berbasis pembuatan kode, tanpa Dynamic CodeGen(IL.Emit)
  • API non-generik tanpa refleksi
  • Deserialisasi ke dalam instance yang ada
  • Serialisasi polimorfisme (Union).
  • dukungan toleran versi terbatas (cepat/default) dan toleran versi penuh
  • Serialisasi referensi melingkar
  • Serialisasi streaming berbasis PipeWriter/Reader
  • Pembuatan kode TypeScript dan ASP.NET Core Formatter
  • Unity(2021.3) Dukungan IL2CPP melalui .NET Source Generator