sách gpt4 ăn đã đi

Xây dựng trình tuần tự hóa nhanh nhất bằng .NET7 và C#11 - lấy MemoryPack làm ví dụ

In lại Tác giả: Tôi là chú chim nhỏ Thời gian cập nhật: 2022-12-02 14:31:26 29 4
mua khóa gpt4 giày nike

Ghi chú của người dịch

Bài viết này là một bài viết hay hiếm có Tác giả của MemoryPack, neuecc, giải thích qua bài viết này cách ông cải thiện hiệu suất của các bộ tuần tự hóa đến mức tối đa; nó giải thích từ nhiều khía cạnh (độ dài thay đổi, chuỗi, bộ sưu tập, v.v.) Một số kỹ thuật tối ưu hóa hiệu suất. đáng để mọi nhà phát triển học hỏi, đặc biệt là các nhà phát triển framework, điều này chắc chắn sẽ mang lại lợi ích cho tất cả mọi người.

Giới thiệu

Tôi đã phát hành một trình tuần tự hóa mới có tên MemoryPack, một trình tuần tự hóa dành riêng cho C# mới hoạt động nhanh hơn nhiều so với các trình tuần tự hóa khác.

So với MessagePack cho C# (bộ tuần tự hóa nhị phân nhanh), hiệu suất của thư viện tuần tự hóa đối tượng tiêu chuẩn nhanh hơn nhiều lần. Khi dữ liệu tối ưu, hiệu suất thậm chí còn nhanh hơn 50 ~ 100 lần. Hỗ trợ tốt nhất là .NET 7, nhưng hiện tại đã có hỗ trợ cho .NET Standard 2.1 (.NET 5, 6), Unity và thậm chí cả TypeScript. Nó cũng hỗ trợ tính đa hình (Union), khả năng chịu lỗi của phiên bản đầy đủ, tham chiếu vòng tròn và các API I/O hiện đại mới nhất (IBufferWriter, ReadOnlySeqeunce, Pipelines).

Hiệu suất của bộ tuần tự hóa dựa trên "đặc tả định dạng dữ liệu" và "việc triển khai trên mỗi ngôn ngữ". Ví dụ: mặc dù các định dạng nhị phân thường có lợi thế hơn các định dạng văn bản như JSON, nhưng trình tuần tự hóa JSON có thể nhanh hơn trình tuần tự hóa nhị phân (như Utf8Json chứng minh). Vậy serializer nhanh nhất là gì? Bộ nối tiếp thực sự nhanh nhất ra đời khi bạn hiểu cả đặc điểm kỹ thuật lẫn cách triển khai.

Trong nhiều năm, tôi đã phát triển và duy trì MessagePack cho C#, một bộ nối tiếp rất thành công trong thế giới .NET với hơn 4000 sao GitHub. Nó cũng đã được áp dụng bởi các sản phẩm tiêu chuẩn của Microsoft như Visual Studio 2022, giao thức SignalR MessagePack Hub và giao thức Blazor Server (blazorpack).

Tôi cũng đã giải quyết gần 1.000 vấn đề trong 5 năm qua. Tôi đã sử dụng trình tạo mã của Roslyn để hỗ trợ AOT từ 5 năm trước và đã chứng minh điều đó, đặc biệt là trong Unity, môi trường AOT (IL2CPP) và nhiều trò chơi di động Unity sử dụng nó.

Ngoài MessagePack cho C#, tôi đã tạo các trình tuần tự hóa như ZeroFormatter (định dạng riêng) và Utf8Json (JSON), đã nhận được nhiều sao GitHub, vì vậy tôi hiểu sâu sắc về đặc điểm hiệu suất của các định dạng khác nhau. Ngoài ra, tôi còn tham gia vào việc tạo khung RPC MagicOnion, cơ sở dữ liệu trong bộ nhớ MasterMemory, ứng dụng khách PubSub AlterNats và ứng dụng khách (Unity)/máy chủ triển khai một số trò chơi.

MemoryPack hướng tới mục tiêu trở thành bộ nối tiếp nhanh, thiết thực và linh hoạt nhất. Tôi nghĩ tôi đã làm được.

Bộ tạo nguồn tăng dần

MemoryPack áp dụng đầy đủ trình tạo nguồn tăng dần nâng cao trong .NET 6. Về cách sử dụng, nó không khác nhiều so với phiên bản C# của MessagePack, ngoại trừ loại mục tiêu được thay đổi thành loại một phần.

                        
                          sử dụng MemoryPack; // Trình tạo nguồn tạo mã tuần tự hóa/hủy tuần tự hóa [MemoryPackable] public partial class Person { public int Age { get; set; } public string Name { get; set; } } // cách sử dụng var v = new Person { Age = 40, Name = "John" }; var bin = MemoryPackSerializer.Serialize(v); var val = MemoryPackSerializer.Deserialize(bin);

                        
                      

Ưu điểm lớn nhất của trình tạo nguồn là thân thiện với AOT, tự động tạo mã tuần tự hóa được tối ưu hóa cho từng loại mà không cần phản chiếu, không cần mã được IL.Emit tạo động, đây là thông lệ. Điều này cho phép sử dụng IL2CPP của Unity để hoạt động an toàn. Tốc độ khởi động ban đầu cũng rất nhanh.

Trình tạo nguồn cũng hoạt động như một trình phân tích cú pháp để nó có thể phát hiện xem việc tuần tự hóa có an toàn hay không bằng cách phát ra các lỗi biên dịch khi chỉnh sửa.

Xin lưu ý rằng vì lý do phiên bản ngôn ngữ/trình biên dịch, phiên bản Unity sử dụng trình tạo nguồn cũ thay vì trình tạo nguồn gia tăng.

Đặc tả nhị phân cho C#

Khẩu hiệu của MemoryPack là "Mã hóa bằng không". Đây không phải là trường hợp đặc biệt; ví dụ, bộ nối tiếp nhị phân chính của Rust, bincode, có đặc điểm kỹ thuật tương tự. FlatBuffers cũng có thể đọc và ghi nội dung tương tự như dữ liệu bộ nhớ mà không yêu cầu triển khai phân tích cú pháp.

Tuy nhiên, không giống như FlatBuffers và các sản phẩm khác, MemoryPack là một bộ tuần tự hóa có mục đích chung không yêu cầu các loại đặc biệt và có thể tuần tự hóa/giải tuần tự hóa đối với POCO. Nó cũng có phiên bản có dung sai cao để bổ sung thành viên lược đồ và hỗ trợ đa hình (Union).

Mã hóa biến và mã hóa cố định

Int32 là 4 byte, nhưng trong JSON chẳng hạn, các số được mã hóa dưới dạng chuỗi có mã hóa độ dài thay đổi từ 1 ~ 11 byte (ví dụ: 1 hoặc -2147483648). Nhiều định dạng nhị phân cũng có thông số kỹ thuật mã hóa có độ dài thay đổi từ 1 đến 5 byte để tiết kiệm kích thước. Ví dụ: loại số Bộ đệm giao thức có mã hóa số nguyên có độ dài thay đổi lưu trữ giá trị trong 7 bit và 1 bit (varint) để lưu trữ sự hiện diện hay vắng mặt của các cờ sau. Điều này có nghĩa là số càng nhỏ thì càng cần ít byte. Thay vào đó, trong trường hợp xấu nhất, con số sẽ tăng lên 5 byte, lớn hơn 4 byte ban đầu. MessagePack và CBOR được xử lý tương tự bằng cách sử dụng mã hóa có độ dài thay đổi, tối thiểu 1 byte cho số nhỏ và tối đa 5 byte cho số lớn.

Điều này có nghĩa là varint thực hiện xử lý bổ sung hơn trường hợp có độ dài cố định. Hãy so sánh cả hai trong mã cụ thể. Độ dài thay đổi là mã hóa biến + zigzag được sử dụng trong protobuf (kết hợp số âm và số dương).

                        
                          // Mã hóa tĩnh đã sửa void WriteFixedInt32(Span bộ đệm, giá trị int) { ref byte p = ref MemoryMarshal.GetReference(bộ đệm); Không an toàn.WriteUnaligned(ref p, giá trị); } // Mã hóa Varint tĩnh void WriteVarInt32(Span bộ đệm, giá trị int) => WriteVarInt64(bộ đệm, giá trị (dài)); static void WriteVarInt64(Span bộ đệm, giá trị dài) { ref byte p = ref MemoryMarshal.GetReference(bộ đệm); ulong n = (ulong)((giá trị << 1) ^ (giá trị >> 63)); while ((n & ~0x7FUL) != 0) { Không an toàn.WriteUnaligned(ref p, (byte)((n & 0x7f) | 0x80)); p = ref Không an toàn.Add(ref p, 1); n >>= 7; } Không an toàn.WriteUnaligned(ref p, (byte)n); }

                        
                      

Nói cách khác, độ dài cố định được ghi ra khỏi bộ nhớ C# nguyên trạng (mã hóa bằng 0) và độ dài cố định rõ ràng là nhanh hơn.

Điều này thậm chí còn rõ ràng hơn khi áp dụng cho mảng.

                        
                          // https://sharplab.io/ Kiểm tra.Heap(số nguyên mới[]{ 1, 2, 3, 4, 5 });

                        
                      

Trong mảng cấu trúc trong C#, dữ liệu được sắp xếp theo thứ tự. Nếu cấu trúc không có loại tham chiếu (loại không được quản lý) thì dữ liệu sẽ được căn chỉnh hoàn toàn trong bộ nhớ; hãy so sánh quá trình tuần tự hóa trong mã với MessagePack và MemoryPack.

                        
                          // Fixed-length(MemoryPack) void Serialize(int[] value) { // Kích thước có thể được tính toán và phân bổ trước var size = (sizeof(int) * value.Length) + 4; EnsureCapacity(size); // MemoryCopy một lần MemoryMarshal.AsBytes(value.AsSpan()).CopyTo(buffer); } // Variable-length(MessagePack) và void Serialize(int[] value) { foreach (var item in value) { // Kích thước không xác định, vì vậy hãy kiểm tra kích thước mỗi lần EnsureCapacity(); // if (buffer.Length < writeLength) Resize(); // Mã hóa độ dài thay đổi cho mỗi phần tử WriteVarInt32(item); } }

                        
                      

Trong trường hợp có độ dài cố định, nhiều lệnh gọi phương thức có thể bị loại bỏ và chỉ có một bản sao của bộ nhớ.

Mảng trong C# không chỉ là các kiểu nguyên thủy như int, mà còn là các cấu trúc có nhiều kiểu nguyên thủy, ví dụ: mảng Vector3 với (float x, float y, float z) sẽ có bố cục bộ nhớ như sau.

Số dấu phẩy động (4 byte) có độ dài cố định là 5 byte trong MessagePack. Thêm 1 byte được thêm tiền tố bằng mã định danh cho biết loại giá trị (số nguyên, số float, chuỗi...). Cụ thể là định dạng [0xca, x, x, x, x, x, x]. Định dạng MemoryPack không có mã định danh nên 4 byte được ghi nguyên trạng.

Lấy Vector3[10000] làm ví dụ, nó tốt hơn 50 lần so với điểm chuẩn.

                        
                          // các trường này tồn tại trong kiểu // byte[] buffer // int offset void SerializeMemoryPack(Vector3[] value) { // chỉ sao chép một lần var size = Unsafe.SizeOf() * 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) { // Lặp lại cho độ dài mảng x số trường foreach (var item in value) { // X { // EnsureCapacity // (Thực tế, tạo danh sách liên kết đệm với bufferWriter.Advance, không phải Resize) if ((buffer.Length - offset) < 5) { Array.Resize(ref buffer, buffer.Length * 2); } var p = MemoryMarshal.GetArrayDataReference(buffer); Không an toàn.WriteUnaligned(ref Không an toàn.Add(ref p, offset), (byte)0xca); Không an toàn.WriteUnaligned(ref Không an toàn.Add(ref p, offset + 1), item.X); offset += 5; } // Y { nếu ((buffer.Length - offset) < 5) { Mảng.Resize(ref buffer, buffer.Length * 2); } var p = MemoryMarshal.GetArrayDataReference(buffer); Không an toàn.WriteUnaligned(ref Không an toàn.Add(ref p, offset), (byte)0xca); Không an toàn.WriteUnaligned(ref Không an toàn.Add(ref p, offset + 1), item.Y); offset += 5; } // Z { nếu ((buffer.Length - offset) < 5) { Mảng.Resize(ref buffer, buffer.Length * 2); } var p = MemoryMarshal.GetArrayDataReference(bộ đệm); Không an toàn.WriteUnaligned(tham chiếu Không an toàn.Thêm(tham chiếu p, độ lệch), (byte)0xca); Không an toàn.WriteUnaligned(tham chiếu Không an toàn.Thêm(tham chiếu p, độ lệch + 1), mục.Z); độ lệch += 5; } } }

                        
                      

Sử dụng MessagePack, nó yêu cầu 30000 cuộc gọi phương thức. Trong phương pháp này, nó kiểm tra xem có đủ bộ nhớ để ghi hay không và thêm phần bù mỗi lần ghi xong.

Với MemoryPack, chỉ có một bản sao của bộ nhớ. Điều này thực sự sẽ thay đổi thời gian xử lý theo một mức độ lớn, đó là lý do tại sao tốc độ tăng tốc 50x~100x được thấy trong hình ở đầu bài viết này.

Tất nhiên, quá trình khử lưu huỳnh cũng là một bản sao duy nhất.

                        
                          // Giải tuần tự hóa MemoryPack, chỉ sao chép Vector3[] DeserializeMemoryPack(ReadOnlySpan bộ đệm, int size) { var dest = new Vector3[size]; MemoryMarshal.Cast(bộ đệm).CopyTo(dest); return dest; } // Yêu cầu đọc float nhiều lần trong vòng lặp Vector3[] DeserializeMessagePack(ReadOnlySpan bộ đệm, int size) { var dest = new Vector3[size]; for (int i = 0; i < size; i++) { var x = ReadSingle(bộ đệm); bộ đệm = bộ đệm.Slice(5); var y = ReadSingle(bộ đệm); bộ đệm = bộ đệm.Slice(5); var z = ReadSingle(bộ đệm); bộ đệm = bộ đệm.Slice(5); dest[i] = new Vector3(x, y, z); } return dest; }

                        
                      

Đây là một hạn chế của bản thân định dạng MessagePack và miễn là tuân theo thông số kỹ thuật thì sự khác biệt lớn về tốc độ sẽ không thể đảo ngược theo bất kỳ cách nào. Tuy nhiên, MessagePack có một đặc tả được gọi là "nhóm định dạng mở rộng" cho phép xử lý đặc biệt các mảng này như một phần đặc tả riêng của nó. Trên thực tế, MessagePack dành cho C# có một tiện ích mở rộng Unity đặc biệt có tên là UnsafeBlitResolver để thực hiện những điều trên.

Tuy nhiên, hầu hết mọi người có thể sẽ không sử dụng nó và sẽ không ai sử dụng các tùy chọn độc quyền khiến MessagePack không tương thích.

Vì vậy, đối với MemoryPack, tôi muốn có C# chuẩn cung cấp hiệu suất tốt nhất theo mặc định.

Tối ưu hóa chuỗi

MemoryPack có hai thông số kỹ thuật chuỗi: UTF8 hoặc UTF16. Vì chuỗi C# là UTF16 nên việc tuần tự hóa chúng thành UTF16 giúp tiết kiệm chi phí mã hóa/giải mã thành UTF8.

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

                        
                      

Tuy nhiên, MemoryPack mặc định là UTF8. Điều này là do vấn đề về kích thước tải trọng; với UTF16, các ký tự ASCII sẽ có kích thước gấp đôi nên UTF8 đã được chọn.

Tuy nhiên, ngay cả với UTF8, MemoryPack có một số tối ưu hóa mà các bộ nối tiếp khác không có.

                        
                          // nhanh 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); } // chậm void WriteUtf8StandardSerializer(string value) { var maxByteCount = Encoding.UTF8.GetByteCount(value); EnsureCapacity(maxByteCount); Encoding.UTF8.GetBytes(value, dest); }

                        
                      

var bytes = Encoding.UTF8.GetBytes(value) hoàn toàn không được phép và việc phân bổ byte[] không được phép khi viết chuỗi. Nhiều trình tuần tự hóa sử dụng Encoding.UTF8.GetByteCount, nhưng cũng nên tránh điều này vì UTF8 là mã hóa có độ dài thay đổi và GetByteCount hoàn toàn lặp qua chuỗi để tính toán kích thước được mã hóa chính xác. Tức là GetByteCount -> GetBytes lặp qua chuỗi hai lần.

Thông thường, bộ nối tiếp được phép giữ lại bộ đệm lớn. Do đó, MemoryPack phân bổ độ dài chuỗi gấp ba lần, đây là trường hợp xấu nhất của mã hóa UTF8, để tránh truyền tải kép. Trong trường hợp giải mã, các tối ưu hóa đặc biệt hơn nữa sẽ được áp dụng.

                        
                          // chuỗi nhanh ReadUtf8MemoryPack(int utf16Length, int utf8Length) { không an toàn { đã sửa (byte* p = &buffer) { trả về chuỗi.Create(utf16Length, ((IntPtr)p, utf8Length), tĩnh (dest, state) => { var src = MemoryMarshal.CreateSpan(ref Unsafe.AsRef((byte*)state.Item1), state.Item2); Utf8.ToUtf16(src, dest, out var bytesRead, out var charsWritten, replaceInvalidSequences: false); }); } } } // chuỗi chậm ReadStandardSerialzier(int utf8Length) { trả về Encoding.UTF8.GetString(buffer.AsSpan(0, utf8Length)); }

                        
                      

Thông thường, để lấy một chuỗi từ byte[], chúng ta sử dụng Encoding.UTF8.GetString(buffer). Nhưng một lần nữa, UTF8 là mã hóa có độ dài thay đổi và chúng tôi không biết độ dài của UTF16. Điều tương tự cũng xảy ra với UTF8. GetString Chúng tôi cần tính toán độ dài dưới dạng UTF16 để chuyển đổi nó thành một chuỗi, do đó, chúng tôi quét chuỗi đó hai lần trong nội bộ. Trong mã giả nó trông như thế này:

                        
                          var length = CalcUtf16Length(utf8data); var str = String.Create(length); Encoding.Utf8.DecodeToString(utf8data, str);

                        
                      

Định dạng chuỗi của trình tuần tự hóa điển hình là UTF8, không thể giải mã thành UTF16, vì vậy ngay cả khi bạn muốn độ dài là UTF16 để giải mã hiệu quả dưới dạng chuỗi C# thì nó không có trong dữ liệu.

Tuy nhiên, MemoryPack ghi lại độ dài UTF16 và độ dài UTF8 trong tiêu đề. Do đó, sự kết hợp của String.Create(Int32, TState, SpanAction) và Utf8.ToUtf16 mang lại khả năng giải mã hiệu quả nhất cho Chuỗi C#.

Về kích thước tải trọng

Mã hóa có độ dài cố định của số nguyên có thể tăng kích thước so với mã hóa có độ dài thay đổi. Tuy nhiên, trong thời hiện đại, việc sử dụng mã hóa có độ dài thay đổi chỉ để giảm kích thước nhỏ của số nguyên là một bất lợi.

Vì dữ liệu không chỉ là số nguyên, nên nếu bạn thực sự muốn giảm kích thước, bạn nên xem xét việc nén ( LZ4 , ZStandard , Brotli , v.v.), mã hóa độ dài thay đổi sẽ không có ý nghĩa gì nếu bạn nén dữ liệu. Nếu bạn muốn chuyên biệt hơn và nhỏ hơn, tính năng nén theo cột sẽ mang lại cho bạn kết quả lớn hơn (ví dụ: Apache Parquet). Để triển khai tính năng nén hiệu quả được tích hợp với MemoryPack, hiện tại tôi có các lớp trợ giúp dành cho BrotliEncode/Decode làm tiêu chuẩn. Tôi cũng có một số thuộc tính áp dụng tính năng nén đặc biệt cho một số cột thô nhất định, chẳng hạn như nén cột.

                        
                          [MemoryPackable] lớp một phần công khai Sample { công khai int Id { lấy; đặt; } [BitPackFormatter] công khai bool[] Dữ liệu { lấy; đặt; } [BrotliFormatter] công khai byte[] Tải trọng { lấy; đặt; } }

                        
                      

BitPackFormatter đại diện cho bool[], bool thường là 1 byte, nhưng vì nó được coi là 1 bit nên tám giá trị boolean được lưu trữ trong một byte. Vậy kích thước xê-ri hóa là 1/8. BrotliFormatter áp dụng thuật toán nén trực tiếp. Điều này thực sự hoạt động tốt hơn việc nén toàn bộ tệp.

Điều này là do không cần bản sao trung gian và quá trình nén có thể được áp dụng trực tiếp vào dữ liệu được tuần tự hóa. Bài viết Giảm chi phí ghi nhật ký theo hai bậc độ lớn với CLP trên Blog Kỹ thuật Uber nêu chi tiết các cách trích xuất hiệu suất và tỷ lệ nén bằng cách áp dụng xử lý theo cách tùy chỉnh dựa trên dữ liệu thay vì nén tổng thể đơn giản.

Sử dụng các tính năng .NET7 và C#11 mới

MemoryPack có các chữ ký phương thức hơi khác nhau khi triển khai .NET Standard 2.1 và triển khai .NET 7. .NET 7 là một triển khai hướng đến hiệu suất, tích cực hơn, tận dụng các tính năng ngôn ngữ mới nhất.

Đầu tiên, giao diện serializer sử dụng các thành viên trừu tượng tĩnh như sau:

                        
                          public interface IMemoryPackable { // lưu ý: tham số serialize phải là `ref readonly` nhưng đặc tả ngôn ngữ hiện tại thì không thể. // xem đề xuất https://github.com/dotnet/csharplang/issues/6010 static abstract void Serialize(ref MemoryPackWriter writer, scoped ref T? value) where TBufferWriter : IBufferWriter; static abstract void Deserialize(ref MemoryPackReader reader, scoped ref T? value); }

                        
                      

MemoryPack sử dụng một trình tạo nguồn và yêu cầu loại mục tiêu là [MemoryPackable]public một phần lớp Foo , vì vậy loại mục tiêu cuối cùng là.

                        
                          [MemortyPackable] lớp một phần Foo: IMemoryPackable { static void IMemoryPackable.Serialize(ref MemoryPackWriter trình ghi, phạm vi tham chiếu Foo? giá trị) { } static void IMemoryPackable.Deserialize(ref MemoryPackReader trình đọc, phạm vi tham chiếu Foo? giá trị) { } }

                        
                      

Điều này tránh được chi phí gọi điện thông qua các phương thức ảo.

                        
                          public void WritePackable(scoped in T? value) where T : IMemoryPackable { // Nếu T là IMemoryPackable, hãy gọi trực tiếp phương thức tĩnh T.Serialize(ref this, ref Unsafe.AsRef(value)); } // public void WriteValue(scoped in T? value) { // gọi Serialize từ giao diện phương thức ảo IMemoryPackFormatter formatter = MemoryPackFormatterProvider.GetFormatter(); formatter.Serialize(ref this, ref Unsafe.AsRef(value)); }

                        
                      

MemoryPackWriter/MemoryPackReader sử dụng trường ref.

                        
                          tham chiếu công khai struct MemoryPackWriter trong đó TBufferWriter : IBufferWriter { tham chiếu TBufferWriter bufferWriter; tham chiếu byte bufferReference; int bufferLength;

                        
                      

Nói cách khác, sự kết hợp của ref byte bufferReference, int bufferLength là nội tuyến của Span. Ngoài ra, bằng cách chấp nhận TBufferWriter làm ref TBufferWriter, giờ đây việc chấp nhận và gọi các cấu trúc có thể thay đổi TBufferWriter:IBufferWrite là an toàn.

                        
                          // bên trong MemoryPack sử dụng một số struct buffer-writers struct BrotliCompressor : IBufferWriter struct FixedArrayBufferWriter : IBufferWriter

                        
                      

Tối ưu hóa cho tất cả các loại

Ví dụ: với cách triển khai chung, các bộ sưu tập có thể được tuần tự hóa/giải tuần tự hóa thành IEnumerable, nhưng MemoryPack cung cấp các cách triển khai riêng cho tất cả các loại. Để đơn giản, List có thể được xử lý như sau:

                        
                          public void Serialize(ref MemoryPackWriter writer, IEnumerable value) { foreach(var item trong nguồn) { writer.WriteValue(item); } } public void Serialize(ref MemoryPackWriter writer, List value) { foreach(var item trong nguồn) { writer.WriteValue(item); } }

                        
                      

Hai mã trông giống nhau nhưng thực thi hoàn toàn khác nhau: foreach tới IEnumerable truy xuất IEnumerator, trong khi foreach tới List truy xuất cấu trúc List.Enumerator, một cấu trúc chuyên biệt, được tối ưu hóa.

Tuy nhiên, MemoryPack còn tối ưu hóa nó hơn nữa.

                        
                          lớp niêm phong công khai ListFormatter : MemoryPackFormatter<>> { ghi đè công khai void Serialize(ref MemoryPackWriter writer, scoped ref List? value) { if (value == null) { writer.WriteNullCollectionHeader(); return; } writer.WriteSpan(CollectionsMarshal.AsSpan(value)); } } // MemoryPackWriter.WriteSpan [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteSpan(scoped Span value) { if (!RuntimeHelpers.IsReferenceOrContainsReferences()) { DangerousWriteUnmanagedSpan(value); return; } var formatter = GetFormatter(); WriteCollectionHeader(value.Length); đối với (int i = 0; i < giá trị. Chiều dài; i++) { trình định dạng. Serialize (tham chiếu này, tham chiếu giá trị [i]); } } // MemoryPackWriter. DangerousWriteUnmanagedSpan [Phương thứcImpl (Phương thứcImplOptions. AggressiveInlining)] public void DangerousWriteUnmanagedSpan  (giá trị Span  có phạm vi) { nếu (giá trị. Chiều dài == 0) { WriteCollectionHeader (0); trả về; } var srcLength = Unsafe. SizeOf  () * giá trị. Chiều dài; var allocSize = srcLength + 4; ref var dest = ref GetSpanReference (allocSize); ref var src = ref Unsafe. As  (tham chiếu MemoryMarshal. GetReference (giá trị)); Unsafe. WriteUnaligned (tham chiếu dest, giá trị. Chiều dài); Không an toàn.CopyBlockUnaligned(ref Không an toàn.Add(ref dest, 4), ref src, (uint)srcLength); Advance(allocSize); }

                        
                      

CollectionsMarshal.AsSpan từ .NET 5 là cách tốt nhất để liệt kê Danh sách. Hơn nữa, nếu có sẵn Span thì nó chỉ có thể được xử lý bằng cách sao chép trong trường hợp Danh sách hoặc Danh sách.

Ngoài ra còn có một số tối ưu hóa thú vị trong trường hợp khử lưu huỳnh. Đầu tiên, quá trình khử lưu huỳnh của MemoryPack chấp nhận tham chiếu đến T? Giá trị, nếu giá trị là null thì nếu được chuyển, nó sẽ ghi đè lên đối tượng được tạo bên trong (giống như một trình tuần tự hóa thông thường). Điều này cho phép không phân bổ việc tạo đối tượng mới trong quá trình khử lưu lượng. Trong trường hợp Danh sách, các bộ sưu tập cũng có thể được sử dụng lại bằng cách gọi Clear().

Sau đó, bằng cách thực hiện lệnh gọi Span đặc biệt, tất cả đều được xử lý dưới dạng Span, tránh chi phí bổ sung của List.Add.

                        
                          public sealed class ListFormatter : MemoryPackFormatter<>> { public override void Deserialize(ref MemoryPackReader reader, scoped ref List? value) { if (!reader.TryReadCollectionHeader(out var length)) { value = null; return; } if (value == null) { value = new List(length); } else if (value.Count == length) { value.Clear(); } var span = CollectionsMarshalEx.CreateSpan(value, length); reader.ReadSpanWithoutReadLengthHeader(length, ref span); } } internal static class CollectionsMarshalEx { ///  /// tương tự như AsSpan nhưng sửa đổi kích thước để tạo khoảng có kích thước cố định. ///  public static Span CreateSpan(List list, int length) { list.EnsureCapacity(length); ref var view = ref Unsafe.As<>, ListView>(ref list); view._size = length; return view._items.AsSpan(0, length); } // LƯU Ý: Các cấu trúc này phụ thuộc vào .NET 7, nếu thay đổi, yêu cầu phải giữ nguyên cấu trúc. internal sealed class ListView { public T[] _items; public int _size; public int _version; } } // MemoryPackReader.ReadSpanWithoutReadLengthHeader public void ReadSpanWithoutReadLengthHeader(int length, scoped ref Span value) { if (length == 0) { value = Array.Empty(); return; } if (!RuntimeHelpers.IsReferenceOrContainsReferences()) { if (value.Length != length) { value = AllocateUninitializedArray(length); } var byteCount = length * Unsafe.SizeOf(); ref var src = ref GetSpanReference(byteCount); ref var dest = ref Unsafe.As(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(); for (int i = 0; i < length; i++) { formatter.Deserialize(ref this, ref value[i]); } } }

                        
                      

EnsureCapacity(capacity) , có thể mở rộng trước kích thước của mảng bên trong chứa Danh sách. Điều này tránh được nhu cầu khuếch đại/sao chép nội bộ mỗi lần.

Nhưng với CollectionsMarshal.AsSpan, bạn sẽ nhận được Span có độ dài 0 vì kích thước bên trong không thay đổi. Nếu chúng ta có CollectionMarshals.AsMemory, chúng ta có thể lấy mảng ban đầu từ đó bằng cách sử dụng kết hợp MemoryMarshal.TryGetArray, nhưng rất tiếc là không có cách nào để lấy mảng ban đầu từ Span. Vì vậy, tôi đã buộc cấu trúc kiểu phải khớp với Unsafe.As và thay đổi List._size và tôi có thể nhận được mảng bên trong mở rộng.

Bằng cách này, chúng tôi có thể tối ưu hóa các loại không được quản lý theo cách chỉ sao chép và tránh List.Add (kiểm tra kích thước mảng mỗi lần) và đóng gói các giá trị qua Span[index] nhanh hơn so với tuần tự hóa truyền thống, deserialization Hiệu suất chương trình cao hơn nhiều. .

Mặc dù các tối ưu hóa cho Danh sách chỉ mang tính đại diện nhưng có quá nhiều loại khác cần đề cập đến, tất cả đều được xem xét cẩn thận và áp dụng các tối ưu hóa tốt nhất cho từng loại.

Tuần tự hóa chấp nhận IBufferWriter làm cấu trúc gốc của nó và quá trình giải tuần tự hóa chấp nhận ReadOnlySpan và ReadOnlySequence.

Điều này là do System.IO.Pipelines yêu cầu những loại này. Nói cách khác, vì nó là nền tảng của máy chủ ASP .NET Core (Kestrel), bạn có thể mong đợi việc tuần tự hóa hiệu suất cao hơn bằng cách kết nối trực tiếp với nó.

IBufferWriter đặc biệt quan trọng vì nó có thể ghi trực tiếp vào bộ đệm, do đó không có bản sao nào trong quá trình tuần tự hóa. Hỗ trợ cho IBufferWriter là điều kiện tiên quyết đối với các bộ tuần tự hóa hiện đại vì nó cung cấp hiệu suất cao hơn so với sử dụng byte[] hoặc Stream. Các bộ tuần tự hóa trong sơ đồ mở (System.Text.Json, protobuf-net, Microsoft.Orleans.Serialization, MessagePack cho C# và MemoryPack) hỗ trợ nó.

MessagePack và MemoryPack

MessagePack dành cho C# rất dễ sử dụng và có hiệu suất tuyệt vời. Đặc biệt, những điều sau đây tốt hơn MemoryPack.

  • Khả năng tương thích đa ngôn ngữ tuyệt vời
  • Khả năng tương thích JSON (đặc biệt là các khóa chuỗi) và khả năng đọc của con người
  • Phiên bản hoàn hảo mặc định có khả năng chịu lỗi
  • Tuần tự hóa các đối tượng và các loại ẩn danh
  • Khử lưu huỳnh động
  • Nén LZ4 nhúng
  • Sự ổn định đã được chứng minh

MemoryPack mặc định có phiên bản giới hạn khả năng chịu lỗi và tùy chọn dung sai phiên bản đầy đủ có hiệu suất thấp hơn một chút. Ngoài ra, vì đây là định dạng thô nên ngôn ngữ duy nhất được hỗ trợ là TypeScript. Ngoài ra, bản thân tệp nhị phân sẽ không cho biết đó là dữ liệu gì vì nó yêu cầu lược đồ C#.

Tuy nhiên, nó vượt trội hơn MessagePack ở những điểm sau.

  • Hiệu suất, đặc biệt đối với mảng thuộc loại không được quản lý
  • Hỗ trợ AOT dễ sử dụng
  • Hàm tạo đa hình mở rộng (kết hợp)
  • Hỗ trợ tham chiếu vòng tròn
  • Ghi đè quá trình khử lưu huỳnh
  • tạo mã bản đánh máy
  • Trình định dạng tùy chỉnh dựa trên thuộc tính linh hoạt

Theo ý kiến ​​​​cá nhân của tôi, nếu bạn ở trong môi trường chỉ có C#, tôi sẽ chọn MemoryPack. Tuy nhiên, khả năng chịu lỗi của phiên bản giới hạn có những điểm kỳ quặc, cần phải hiểu trước. MessagePack cho C# vẫn là một lựa chọn tốt vì tính đơn giản và dễ sử dụng của nó.

MemoryPack không phải là một trình tuần tự hóa thử nghiệm chỉ tập trung vào hiệu suất mà còn được coi là một trình tuần tự hóa thực tế. Vì mục đích này, tôi cũng đã cung cấp nhiều tính năng dựa trên trải nghiệm của tôi với MessagePack cho C#.

  • Hỗ trợ API I/O hiện đại ( IBufferWriter Khoảng thời gian chỉ đọc Trình tự chỉ đọc )
  • Tạo mã dựa trên trình tạo nguồn thân thiện với AOT gốc, không tạo mã động (IL.Emit)
  • API không chung chung không phản chiếu
  • Giải tuần tự hóa thành phiên bản hiện có
  • Tuần tự hóa đa hình (liên minh)
  • Dung sai phiên bản giới hạn (nhanh/mặc định) và hỗ trợ dung sai phiên bản đầy đủ
  • tuần tự hóa tham chiếu vòng tròn
  • Tuần tự hóa phát trực tuyến dựa trên trình ghi/đọc ống
  • Tạo mã TypeScript và định dạng lõi ASP.NET
  • Unity (2021.3) hỗ trợ IL2CPP thông qua trình tạo nguồn .NET

Chúng tôi dự định mở rộng hơn nữa phạm vi tính năng sẵn có, chẳng hạn như hỗ trợ MemoryPack cho MasterMemory và hỗ trợ thay đổi bộ nối tiếp cho MagicOnion. Chúng tôi định vị mình là cốt lõi của hệ sinh thái thư viện Cysharp C#. Chúng tôi sẽ nỗ lực rất nhiều để trồng cây này, vì vậy, trước tiên, hãy thử thư viện của chúng tôi! .

Thông tin bản quyền

Sự cho phép đã được lấy từ tác giả gốc. Bản quyền gốc: neuecc Bản quyền dịch: InCerry.

Liên kết gốc: https://neuecc.medium.com/how-to-make-the-fastest-net-serializer-with-net-7-c-11-case-of-memorypack-ad28c0366516.

Nhóm trao đổi tối ưu hóa hiệu suất .NET

Tôi tin rằng mọi người thường gặp phải một số vấn đề về hiệu suất trong quá trình phát triển. Họ thiếu các công cụ hiệu quả để tìm ra tắc nghẽn về hiệu suất hoặc họ không biết cách tối ưu hóa sau khi phát hiện ra tắc nghẽn. Độc giả và bạn bè luôn hỏi có nhóm trao đổi kỹ thuật nào không, nhưng vì nhiều lý do mà nhóm này chưa bao giờ được thành lập. Bây giờ tôi vui mừng thông báo ở đây rằng tôi đã tạo một nhóm chuyên trao đổi kinh nghiệm tối ưu hóa hiệu suất .NET. nhưng không giới hạn ở:

  • Cách tìm ra các điểm nghẽn về hiệu suất của .NET, chẳng hạn như sử dụng APM, công cụ dotnet và các công cụ khác
  • Triển khai các nguyên tắc cơ bản của .NET framework, chẳng hạn như trình thu gom rác, JIT, v.v.
  • Cách viết mã .NET hiệu suất cao và những cạm bẫy về hiệu suất

Tôi hy vọng nhiều người bạn cùng chí hướng hơn có thể tham gia cùng chúng tôi và chia sẻ một số vấn đề về hiệu suất .NET gặp phải trong công việc cũng như kinh nghiệm tối ưu hóa và phân tích hiệu suất có giá trị. Vì nó đã đạt 200 người nên bạn có thể thêm tôi trên WeChat và tôi sẽ thêm bạn vào nhóm: ls1075.

Cuối cùng, bài viết này về cách sử dụng .NET7 và C#11 để tạo trình tuần tự hóa nhanh nhất - lấy MemoryPack làm ví dụ, kết thúc tại đây. Nếu bạn muốn biết thêm về cách sử dụng .NET7 và C#11 để tạo chương trình tuần tự hóa nhanh nhất - hãy tham khảo. MemoryPack là một ví dụ, vui lòng tìm kiếm các bài viết của CFSDN hoặc tiếp tục duyệt các bài viết liên quan. Tôi hy vọng bạn sẽ ủng hộ blog của tôi trong tương lai! .

29 4 0
tôi là một con chim nhỏ
Hồ sơ

Tôi là một lập trình viên xuất sắc, rất giỏi!

Nhận phiếu giảm giá taxi Didi miễn phí
Phiếu giảm giá taxi Didi
Chứng chỉ ICP Bắc Kinh số 000000
Hợp tác quảng cáo: 1813099741@qq.com 6ren.com
Xem sitemap của VNExpress