วิธีสร้าง .NET Serializer ที่เร็วที่สุดด้วย .NET 7 / C# 11 กรณีของ MemoryPack
ฉันได้เปิดตัว serializer ใหม่ชื่อMemoryPackซึ่งเป็น serializer เฉพาะ C # ใหม่ที่ทำงานเร็วกว่า serializers อื่น ๆ
เมื่อเปรียบเทียบกับMessagePack สำหรับ C#ซึ่งเป็นไบนารีซีเรียลไลเซอร์ที่รวดเร็ว ประสิทธิภาพการทำงานจะเร็วขึ้นหลายเท่าสำหรับอ็อบเจ็กต์มาตรฐาน และเร็วกว่า 50~100 เท่าเมื่อข้อมูลเหมาะสมที่สุด การสนับสนุนที่ดีที่สุดคือ .NET 7 แต่ตอนนี้รองรับ .NET Standard 2.1 (.NET 5, 6), Unity และแม้แต่ TypeScript นอกจากนี้ยังรองรับ Polymorphism(Union), รองรับเวอร์ชันเต็ม, การอ้างอิงแบบวงกลม และ I/O API ที่ทันสมัยล่าสุด (IBufferWriter, ReadOnlySeqeunce, Pipelines)
ประสิทธิภาพของ Serializer ขึ้นอยู่กับทั้ง "ข้อมูลจำเพาะของรูปแบบข้อมูล" และ "การใช้งานในแต่ละภาษา" ตัวอย่างเช่น แม้ว่าโดยทั่วไปแล้วรูปแบบไบนารีจะได้เปรียบกว่ารูปแบบข้อความ (เช่น JSON) แต่ก็เป็นไปได้ที่จะมีตัวซีเรียลไลเซอร์ JSON ที่เร็วกว่าตัวซีเรียลไลเซอร์แบบไบนารี (ตามที่แสดงด้วยUtf8Json ) Serializer ที่เร็วที่สุดคืออะไร? เมื่อคุณลงลึกทั้งข้อมูลจำเพาะและการใช้งาน โปรแกรมซีเรียลไลเซอร์ที่เร็วที่สุดอย่างแท้จริงก็ถือกำเนิดขึ้น
ฉันพัฒนาและบำรุงรักษา MessagePack สำหรับ C# มาหลายปีแล้ว และ MessagePack สำหรับ C# เป็นโปรแกรมสร้างซีเรียลไลเซอร์ที่ประสบความสำเร็จอย่างมากในโลก .NET โดยมีดาว GitHub กว่า 4,000 ดวง นอกจากนี้ยังถูกนำมาใช้โดยผลิตภัณฑ์มาตรฐานของ Microsoft เช่น Visual Studio 2022, SignalR MessagePack Hub Protocolและโปรโตคอล Blazor Server (blazorpack)
ในช่วง 5 ปีที่ผ่านมา ฉันได้ประมวลผลเกือบ 1,000 เรื่องแล้ว ฉันได้ทำงานเกี่ยวกับการสนับสนุน AOT ด้วยตัวสร้างรหัสโดยใช้ Roslyn ตั้งแต่ 5 ปีที่แล้ว และได้แสดงให้เห็นแล้ว โดยเฉพาะอย่างยิ่งใน Unity, สภาพแวดล้อม AOT (IL2CPP) และเกมมือถือ Unity หลายเกมที่ใช้มัน
นอกจาก MessagePack สำหรับ C# แล้ว ฉันยังสร้างซีเรียลไลเซอร์ เช่นZeroFormatter (รูปแบบของตัวเอง) และUtf8Json (JSON) ซึ่งได้รับดาว GitHub จำนวนมาก ดังนั้นฉันจึงมีความเข้าใจอย่างลึกซึ้งเกี่ยวกับลักษณะการทำงานของรูปแบบต่างๆ นอกจากนี้ ฉันได้มีส่วนร่วมในการสร้างเฟรมเวิร์ก RPC MagicOnionฐานข้อมูลในหน่วยความจำMasterMemoryไคลเอ็นต์ PubSub AlterNatsและการใช้งานทั้งไคลเอนต์ (Unity)/เซิร์ฟเวอร์ ของเกมต่างๆ
เป้าหมายของ MemoryPack คือการเป็นเครื่องซีเรียลไลเซอร์อเนกประสงค์ที่รวดเร็ว ใช้งานได้จริง และใช้งานได้หลากหลาย และฉันคิดว่าฉันทำสำเร็จแล้ว
ตัวสร้างแหล่งที่มาที่เพิ่มขึ้น
MemoryPack ใช้Incremental Source Generator ที่ปรับปรุงใน .NET 6 อย่างสมบูรณ์ ในแง่ของการใช้งาน ก็ไม่แตกต่างจาก MessagePack สำหรับ C# มากนัก ยกเว้นการเปลี่ยนประเภทเป้าหมายเป็นบางส่วน
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);
Source Generator ยังทำหน้าที่เป็นตัววิเคราะห์ ดังนั้นจึงสามารถตรวจจับได้ว่าสามารถจัดลำดับได้อย่างปลอดภัยหรือไม่โดยการแจ้งข้อผิดพลาดในการคอมไพล์ ณ เวลาแก้ไข
โปรดทราบว่าเวอร์ชัน Unity ใช้Source Generator เก่าแทน Incremental Source Generator เนื่องจากเหตุผลด้านภาษา/เวอร์ชันคอมไพเลอร์
ข้อกำหนดไบนารีสำหรับ C #
สโลแกนของ MemoryPack คือ “Zero encoding” นี่ไม่ใช่เรื่องราวพิเศษ ไบนารีซีเรียลไลเซอร์หลักของ Rust เช่นbincodeมีลักษณะเฉพาะที่คล้ายกัน FlatBuffersยังอ่านและเขียนเนื้อหาที่คล้ายกับข้อมูลหน่วยความจำโดยไม่ต้องแยกวิเคราะห์การใช้งาน
อย่างไรก็ตาม ไม่เหมือนกับ FlatBuffers และอื่นๆ MemoryPack เป็นโปรแกรมซีเรียลไลเซอร์สำหรับวัตถุประสงค์ทั่วไปที่ไม่ต้องการประเภทพิเศษและซีเรียลไลซ์/ดีซีเรียลไลซ์กับ POCO นอกจากนี้ยังมีการกำหนดเวอร์ชันที่ทนทานต่อการเพิ่มสมาชิกสคีมาและการสนับสนุนความหลากหลาย (Union)
การเข้ารหัส varint เทียบกับการแก้ไข
Int32 คือ 4 ไบต์ แต่ใน JSON ตัวอย่างเช่น ตัวเลขจะถูกเข้ารหัสเป็นสตริงที่มีการเข้ารหัสความยาวผันแปรได้ที่ 1~11 ไบต์ (เช่น 1 หรือ -2147483648) รูปแบบไบนารีจำนวนมากยังมีข้อกำหนดการเข้ารหัสความยาวผันแปรได้ตั้งแต่ 1 ถึง 5 ไบต์เพื่อบันทึกขนาด ตัวอย่างเช่นประเภทตัวเลขของ Protocol Buffersมีการเข้ารหัสจำนวนเต็มความยาวผันแปรได้ที่เก็บค่าใน 7 บิตและแฟล็กสำหรับการมีหรือไม่มีต่อไปนี้ใน 1 บิต (varint) ซึ่งหมายความว่ายิ่งจำนวนน้อยเท่าใดก็ยิ่งต้องการไบต์น้อยลงเท่านั้น ในทางกลับกัน ในกรณีที่เลวร้ายที่สุด จำนวนจะเพิ่มขึ้นเป็น 5 ไบต์ ซึ่งมากกว่าเดิม 4 ไบต์ MessagePackและCBORได้รับการประมวลผลในทำนองเดียวกันโดยใช้การเข้ารหัสความยาวผันแปรได้ โดยมีอย่างน้อย 1 ไบต์สำหรับตัวเลขจำนวนน้อย และสูงสุด 5 ไบต์สำหรับตัวเลขจำนวนมาก
ซึ่งหมายความว่า varint ถูกเรียกใช้การประมวลผลเพิ่มเติมกว่าในกรณีที่มีความยาวคงที่ ลองเปรียบเทียบทั้งสองในรหัสคอนกรีต ความยาวแปรผันคือการเข้ารหัส varint + ZigZag (รวมจำนวนลบและบวก) ที่ใช้ในโปรโตบัฟ
// 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);
}
สิ่งนี้ยิ่งเด่นชัดมากขึ้นเมื่อนำไปใช้กับอาร์เรย์
// 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);
}
}
อาร์เรย์ใน C# ไม่เพียงแต่เป็นประเภทดั้งเดิมเช่น int เท่านั้น แต่ยังเป็นจริงสำหรับโครงสร้างที่มีองค์ประกอบดั้งเดิมหลายอย่าง เช่น อาร์เรย์ Vector3 ที่มี (float x, float y, float z) จะมีเค้าโครงหน่วยความจำดังต่อไปนี้
โฟลต (4 ไบต์) คือความยาวคงที่ 5 ไบต์ใน MessagePack เพิ่มอีก 1 ไบต์นำหน้าด้วยตัวระบุที่ระบุว่าค่าประเภทใด (Int, Float, String…) โดยเฉพาะ [0xca, x, x, x, x, x] รูปแบบ MemoryPack ไม่มีตัวระบุ ดังนั้น 4 ไบต์จึงเขียนตามที่เป็นอยู่
พิจารณา Vector3[10000] ซึ่งดีกว่าเกณฑ์มาตรฐานถึง 50 เท่า
// 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;
}
}
}
ด้วย MemoryPack เพียงสำเนาหน่วยความจำเดียว สิ่งนี้จะเปลี่ยนเวลาในการประมวลผลตามลำดับความสำคัญและเป็นสาเหตุของการเร่งความเร็ว 50x~100x ในกราฟที่จุดเริ่มต้นของบทความนี้
แน่นอน กระบวนการดีซีเรียลไลเซชันก็เป็นสำเนาเดียวเช่นกัน
// 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 ไม่สามารถใช้งานร่วมกันได้
ดังนั้นด้วย MemoryPack ฉันจึงต้องการข้อกำหนดที่จะให้ประสิทธิภาพที่ดีที่สุดเป็น C# โดยค่าเริ่มต้น
การเพิ่มประสิทธิภาพสตริง
MemoryPack มีสองข้อกำหนดสำหรับสตริง: UTF8 หรือ UTF16 เนื่องจากสตริง C# เป็น UTF16 การทำให้เป็นอันดับเป็น UTF16 จึงช่วยประหยัดค่าใช้จ่ายในการเข้ารหัส/ถอดรหัสเป็น 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);
}
อย่างไรก็ตาม แม้จะใช้ 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);
}
โดยปกติแล้ว serializers จะได้รับอนุญาตให้สำรองบัฟเฟอร์จำนวนมาก ดังนั้น 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));
}
var length = CalcUtf16Length(utf8data);
var str = String.Create(length);
Encoding.Utf8.DecodeToString(utf8data, str);
อย่างไรก็ตาม MemoryPack จะบันทึกทั้งความยาว UTF16 และ UTF8 ในส่วนหัว ดังนั้น การรวมกันของ String.Create<TState>(Int32, TState, SpanAction<Char,TState>) และ Utf8.ToUtf16 ให้การถอดรหัส C# String มีประสิทธิภาพสูงสุด
เกี่ยวกับขนาดน้ำหนักบรรทุก
การเข้ารหัสความยาวคงที่ของจำนวนเต็มอาจมีขนาดที่สูงเกินจริงเมื่อเทียบกับการเข้ารหัสที่มีความยาวผันแปรได้ อย่างไรก็ตาม ในยุคปัจจุบัน การใช้การเข้ารหัสความยาวผันแปรเพียงเพื่อลดขนาดจำนวนเต็มขนาดเล็กนั้นเป็นข้อเสียมากกว่า
เนื่องจากข้อมูลไม่ได้เป็นเพียงจำนวนเต็ม หากคุณต้องการลดขนาดจริง ๆ คุณควรพิจารณาการบีบอัด ( LZ4 , ZStandard , Brotliและอื่น ๆ ) และหากคุณบีบอัดข้อมูล แทบจะไม่มีประโยชน์ในการเข้ารหัสความยาวผันแปร หากคุณต้องการเฉพาะเจาะจงมากขึ้นและมีขนาดเล็กลง การบีบอัดเชิงคอลัมน์จะให้ผลลัพธ์ที่ดีกว่า (เช่นApache Parquet )
สำหรับการบีบอัดที่มีประสิทธิภาพที่รวมเข้ากับการใช้งาน 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; }
}
BrotliFormatterใช้อัลกอริธึมการบีบอัดโดยตรง สิ่งนี้ทำงานได้ดีกว่าการบีบอัดไฟล์ทั้งหมด
นี่เป็นเพราะไม่จำเป็นต้องทำสำเนาระหว่างกลางและกระบวนการบีบอัดสามารถนำไปใช้โดยตรงกับข้อมูลที่ทำให้เป็นอนุกรมได้
วิธีการแยกประสิทธิภาพและอัตราส่วนการบีบอัดโดยการใช้การประมวลผลด้วยวิธีที่กำหนดเองโดยขึ้นอยู่กับข้อมูล แทนที่จะเป็นการบีบอัดโดยรวมอย่างง่าย มีรายละเอียดอยู่ใน บทความ การลดต้นทุนการบันทึกโดยสองคำสั่งของขนาดโดยใช้ CLPในบล็อกวิศวกรรมของ Uber
การใช้คุณสมบัติใหม่ .NET 7 / C#11
MemoryPack มีลายเซ็นของเมธอดที่แตกต่างกันเล็กน้อยในการใช้งานสำหรับ .NET Standard 2.1 และการใช้งานสำหรับ .NET 7 .NET 7 เป็นการใช้งานที่มุ่งเน้นประสิทธิภาพในเชิงรุกมากกว่า ซึ่งใช้ประโยชน์จากฟีเจอร์ภาษาล่าสุด
ขั้นแรก อินเทอร์เฟซ serializer ใช้สมาชิกนามธรรมแบบคงที่ดังต่อไปนี้
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>
ตัวอย่างเช่น คอลเลกชันสามารถซีเรียลไลซ์/ดีซีเรียลไลซ์เป็น 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);
}
}
อย่างไรก็ตาม 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);
}
ในกรณีของ Deserialize มีการเพิ่มประสิทธิภาพที่น่าสนใจเช่นกัน ประการแรก Deserialize ของ MemoryPack ยอมรับการอ้างอิง T? ค่า และถ้าค่าเป็น null มันจะเขียนทับวัตถุที่สร้างขึ้นภายใน (เช่นเดียวกับซีเรียลไลเซอร์ปกติ) ถ้าค่านั้นถูกส่งผ่าน สิ่งนี้ทำให้ไม่มีการจัดสรรการสร้างวัตถุใหม่ระหว่าง Deserialize คอลเลกชันยังใช้ซ้ำได้ด้วยการเรียก Clear() ในกรณีของ List<T>
จากนั้น ด้วยการเรียกใช้ 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]);
}
}
}
แต่ CollectionsMarshal.AsSpan คุณจะได้ช่วงความยาว 0 เนื่องจากขนาดภายในไม่เปลี่ยนแปลง หากเรามี CollectionMarshals.AsMemory เราสามารถรับอาร์เรย์ดิบจากที่นั่นด้วยคอมโบ MemoryMarshal.TryGetArray แต่น่าเสียดายที่ไม่มีวิธีรับอาร์เรย์ดั้งเดิมจาก Span ดังนั้นฉันจึงบังคับให้โครงสร้างประเภทตรงกับ Unsafe.As และเปลี่ยน List<T>._size ฉันจึงสามารถรับอาร์เรย์ภายในที่ขยายได้
ด้วยวิธีนี้ เราสามารถเพิ่มประสิทธิภาพให้กับประเภทที่ไม่มีการจัดการเพื่อคัดลอก และหลีกเลี่ยง List<T>.Add (ซึ่งจะตรวจสอบขนาดอาร์เรย์ในแต่ละครั้ง) และแพ็คค่าผ่าน Span<T>[index] ซึ่งสูงกว่ามาก การดีซีเรียลไลเซชันของซีเรียลไลเซอร์แบบธรรมดา ประสิทธิภาพ.
ในขณะที่การเพิ่มประสิทธิภาพให้กับ List<T> นั้นเป็นตัวแทน แต่ก็มีอื่นๆ อีกมากเกินกว่าที่จะแนะนำ ทุกประเภทได้รับการพิจารณาอย่างถี่ถ้วนและปรับใช้การเพิ่มประสิทธิภาพที่ดีที่สุดเท่าที่จะเป็นไปได้กับแต่ละประเภท
Serialize ยอมรับ IBufferWriter<byte> เป็นโครงสร้างดั้งเดิม และ Deserialize ยอมรับ ReadOnlySpan<byte> และ ReadOnlySequence<byte>
นี่เป็นเพราะประเภทเหล่านี้จำเป็นโดยSystem.IO.Pipelines กล่าวอีกนัยหนึ่ง เนื่องจากเป็นพื้นฐานของ ASP .NET Core's Server (Kestrel) คุณจึงสามารถคาดหวังถึงประสิทธิภาพที่สูงขึ้นได้โดยการเชื่อมต่อกับเซิร์ฟเวอร์โดยตรง
IBufferWriter<byte> มีความสำคัญเป็นพิเศษเนื่องจากสามารถเขียนไปยังบัฟเฟอร์ได้โดยตรง ทำให้ได้สำเนาเป็นศูนย์ในกระบวนการทำให้เป็นอนุกรม การสนับสนุนสำหรับ IBufferWriter<byte> เป็นข้อกำหนดเบื้องต้นสำหรับ serializers สมัยใหม่ เนื่องจากให้ประสิทธิภาพสูงกว่าการใช้ byte[] หรือ Stream Serializer สำหรับกราฟที่จุดเริ่มต้น (System.Text.Json, protobuf-net, Microsoft.Orleans.Serialization, MessagePack for C# และ MemoryPack) รองรับ
MessagePack กับ MemoryPack
MessagePack สำหรับ C# นั้นใช้งานง่ายและมีประสิทธิภาพดีเยี่ยม โดยเฉพาะอย่างยิ่ง ประเด็นต่อไปนี้ดีกว่า MemoryPack
- ความเข้ากันได้ระหว่างภาษาที่ยอดเยี่ยม
- ความเข้ากันได้ของ JSON (โดยเฉพาะอย่างยิ่งสำหรับคีย์สตริง) และความสามารถในการอ่านของมนุษย์
- รุ่นที่สมบูรณ์แบบทนทานโดยค่าเริ่มต้น
- การทำให้เป็นอนุกรมของวัตถุและประเภทนิรนาม
- ดีซีเรียลไลเซชันแบบไดนามิก
- ฝังการบีบอัด LZ4
- พิสูจน์ความเสถียรมาอย่างยาวนาน
อย่างไรก็ตาม มันเหนือกว่า MessagePack ด้วยวิธีการต่อไปนี้
- ประสิทธิภาพ โดยเฉพาะอย่างยิ่งสำหรับอาร์เรย์ประเภทที่ไม่มีการจัดการ
- การสนับสนุน AOT ที่ใช้งานง่าย
- วิธีการสร้าง Extended Polymorphism (Union)
- รองรับการอ้างอิงแบบวงกลม
- เขียนทับ deserialization
- การสร้างรหัส TypeScript
- รูปแบบที่กำหนดเองตามแอตทริบิวต์ที่ยืดหยุ่น
MemoryPack ไม่ใช่ซีเรียลไลเซอร์รุ่นทดลองที่เน้นประสิทธิภาพเท่านั้น แต่ยังตั้งใจให้เป็นซีเรียลไลเซอร์ที่ใช้งานได้จริงด้วย ด้วยเหตุนี้ ฉันยังได้สั่งสมประสบการณ์ของฉันกับ MessagePack สำหรับ C# เพื่อมอบคุณลักษณะต่างๆ มากมาย
- รองรับ I/O API สมัยใหม่ (`IBufferWriter<byte>`, `ReadOnlySpan<byte>`, `ReadOnlySequence<byte>`)
- การสร้างรหัสที่ใช้ตัวสร้างซอร์สที่เป็นมิตรกับ AOT ดั้งเดิมไม่มี Dynamic CodeGen (IL.Emit)
- API ที่ไม่ใช่แบบทั่วไปที่ไม่มีการสะท้อนกลับ
- ยกเลิกการทำให้เป็นอนุกรมในอินสแตนซ์ที่มีอยู่
- การทำให้เป็นอนุกรมแบบ Polymorphism (Union)
- รองรับเวอร์ชันที่จำกัด (เร็ว/ดีฟอลต์) และรองรับเวอร์ชันเต็ม
- การทำให้เป็นอนุกรมการอ้างอิงแบบวงกลม
- การทำให้เป็นอันดับการสตรีมตาม PipeWriter / Reader
- การสร้างรหัส TypeScript และ ASP.NET Core Formatter
- Unity(2021.3) รองรับ IL2CPP ผ่าน .NET Source Generator