使用.NET7和C#11打造最快的序列化程序-以MemoryPack為例

譯者注

本文是一篇不可多得的好文,MemoryPack 的作者 neuecc 大佬通過本文解釋了他是如何將序列化程序性能提升到極致的;其中從很多方面(可變長度、字符串、集合等)解釋了一些性能優化的技巧,值得每一個開發人員學習,特別是框架的開發人員的學習,一定能讓大家獲益匪淺。

由于公眾號排版原因,譯者建議大家在桌面端閱讀本文,手機閱讀體驗并不是很好。

簡介

我發布了一個名為MemoryPack[1] 的新序列化程序,這是一種特定于 C# 的新序列化程序,其執行速度比其他序列化程序快得多。

5dd8958f643db7f5639539918e61fdb8.png

與MessagePack for C#[2] (一個快速的二進制序列化程序)相比標準對象的序列化庫性能快幾倍,當數據最優時,性能甚至快 50~100 倍。最好的支持是.NET 7,但現在支持.NET Standard 2.1(.NET 5,6),Unity 甚至 TypeScript。它還支持多態性(Union),完整版本容錯,循環引用和最新的現代 I/O API(IBufferWriter,ReadOnlySeqeunce,Pipelines)。

序列化程序的性能基于“數據格式規范”和“每種語言的實現”。例如,雖然二進制格式通常比文本格式(如 JSON)具有優勢,但 JSON 序列化程序可能比二進制序列化程序更快(如Utf8Json[3] 所示)。那么最快的序列化程序是什么?當你同時了解規范和實現時,真正最快的序列化程序就誕生了。

多年來,我一直在開發和維護 MessagePack for C#,而 MessagePack for C# 是 .NET 世界中非常成功的序列化程序,擁有超過 4000 顆 GitHub 星。它也已被微軟標準產品采用,如 Visual Studio 2022,SignalR MessagePack Hub[4]協議和 Blazor Server 協議(blazorpack)。

在過去的 5 年里,我還處理了近 1000 個問題。自 5 年前以來,我一直在使用 Roslyn 的代碼生成器進行 AOT 支持,并對其進行了演示,尤其是在 Unity、AOT 環境 (IL2CPP) 以及許多使用它的 Unity 手機游戲中。

除了 MessagePack for C# 之外,我還創建了ZeroFormatter[5](自己的格式)和Utf8Json[6](JSON)等序列化程序,它們獲得了許多 GitHub Star,所以我對不同格式的性能特征有深刻的理解。此外,我還參與了 RPC 框架MagicOnion[7],內存數據庫MasterMemory[8],PubSub 客戶端AlterNats[9]以及幾個游戲的客戶端(Unity)/服務器實現的創建。

MemoryPack 的目標是成為終極的快速,實用和多功能的序列化程序。我想我做到了。

增量源生成器

MemoryPack 完全采用 .NET 6 中增強的增量源生成器[10]。在用法方面,它與 C# 版 MessagePack 沒有太大區別,只是將目標類型更改為部分類型。

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

源生成器的最大優點是它對 AOT 友好,無需反射即可為每種類型自動生成優化的序列化程序代碼,而無需由 IL.Emit 動態生成代碼,這是常規做法。這使得使用 Unity 的 IL2CPP 等可以安全地工作。初始啟動速度也很快。

3fbd86c688f50dd8712b780c5a2f6c00.png

源生成器還用作分析器,因此它可以通過在編輯時發出編譯錯誤來檢測它是否可安全序列化。

請注意,由于語言/編譯器版本原因,Unity 版本使用舊的源生成器[11]而不是增量源生成器。

C# 的二進制規范

MemoryPack 的標語是“零編碼”。這不是一個特例,例如,Rust 的主要二進制序列化器bincode[12] 也有類似的規范。FlatBuffers[13]還可以讀取和寫入類似于內存數據的內容,而無需解析實現。

但是,與 FlatBuffers 和其他產品不同,MemoryPack 是一種通用的序列化程序,不需要特殊類型,并且可以針對 POCO 進行序列化/反序列化。它還具有對架構成員添加和多態性支持 (Union) 的高容忍度的版本控制。

可變編碼與固定編碼

Int32 是 4 個字節,但在 JSON 中,例如,數字被編碼為字符串,可變長度編碼為 1~11 個字節(例如,1 或 -2147483648)。許多二進制格式還具有 1 到 5 字節的可變長度編碼規范以節省大小。例如,Protocol-buffers 數字類型[14]具有可變長度整數編碼,該編碼以 7 位存儲值,并以 1 位 (varint) 存儲是否存在以下的標志。這意味著數字越小,所需的字節就越少。相反,在最壞的情況下,該數字將增長到 5 個字節,大于原來的 4 個字節。MessagePack[15]和CBOR[16]類似地使用可變長度編碼進行處理,小數字最小為 1 字節,大數字最大為 5 字節。

這意味著 varint 運行比固定長度情況額外的處理。讓我們在具體代碼中比較兩者。可變長度是 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);
}

換句話說,固定長度是按原樣寫出 C# 內存(零編碼),很明顯,固定長度更快。

當應用于數組時,這一點更加明顯。

//?https://sharplab.io/
Inspect.Heap(new?int[]{?1,?2,?3,?4,?5?});
b0a4ca0e2aa71bac23f80190f382627d.png

在 C# 中的結構數組中,數據按順序排列。如果結構沒有引用類型(非托管類型)[17]則數據在內存中完全對齊;讓我們將代碼中的序列化過程與 MessagePack 和 MemoryPack 進行比較。

//?Fixed-length(MemoryPack)
void?Serialize(int[]?value)
{//?Size?can?be?calculated?and?allocate?in?advancevar?size?=?(sizeof(int)?*?value.Length)?+?4;EnsureCapacity(size);//?MemoryCopy?onceMemoryMarshal.AsBytes(value.AsSpan()).CopyTo(buffer);
}// Variable-length(MessagePack)
void?Serialize(int[]?value)
{foreach?(var?item?in?value){//?Unknown?size,?so?check?size?each?timesEnsureCapacity();?//?if?(buffer.Length?<?writeLength)?Resize();//?Variable?length?encoding?per?elementWriteVarInt32(item);}
}

在固定長度的情況下,可以消除許多方法調用并且只有一個內存副本。

C# 中的數組不僅是像 int 這樣的基元類型,對于具有多個基元的結構也是如此,例如,具有 (float x, float y, float z) 的 Vector3 數組將具有以下內存布局。

1bea7599e6494a7b7e29c65649d345cc.png

浮點數(4 字節)是 MessagePack 中 5 個字節的固定長度。額外的 1 個字節以標識符為前綴,指示值的類型(整數、浮點數、字符串...)。具體來說,[0xca, x, x, x, x, x, x].MemoryPack 格式沒有標識符,因此 4 個字節按原樣寫入。

以 Vector3[10000] 為例,它比基準測試好 50 倍。

//?these?fields?exists?in?type
//?byte[]?buffer
//?int?offsetvoid?SerializeMemoryPack(Vector3[]?value)
{//?only?do?copy?oncevar?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?fieldsforeach?(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;}}
}

使用 MessagePack,它需要 30000 次方法調用。在該方法中,它會檢查是否有足夠的內存進行寫入,并在每次完成寫入時添加偏移量。

使用 MemoryPack,只有一個內存副本。這實際上會使處理時間改變一個數量級,這也是本文開頭圖中 50 倍~100 倍加速的原因。

當然,反序列化過程也是單個副本。

//?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 格式本身的限制,只要遵循規范,速度的巨大差異就無法以任何方式逆轉。但是,MessagePack 有一個名為“ext 格式系列”的規范,它允許將這些數組作為其自身規范的一部分進行特殊處理。事實上,MessagePack for C# 有一個特殊的 Unity 擴展選項,稱為 UnsafeBlitResolver,它可以執行上述操作。

但是,大多數人可能不會使用它,也沒有人會使用會使 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>?->?CopyMemoryMarshal.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);
}

但是,MemoryPack 默認為 UTF8。這是因為有效負載大小問題;對于 UTF16,ASCII 字符的大小將是原來的兩倍,因此選擇了 UTF8。

但是,即使使用 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);
}

var bytes = Encoding.UTF8.GetBytes(value)是絕對的不允許的,字符串寫入中不允許 byte[] 分配。許多序列化程序使用 Encoding.UTF8.GetByteCount,但也應該避免它,因為 UTF8 是一種可變長度編碼,GetByteCount 完全遍歷字符串以計算確切的編碼后大小。也就是說,GetByteCount -> GetBytes 遍歷字符串兩次。

通常,允許序列化程序保留大量緩沖區。因此,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));
}

通常,要從 byte[] 中獲取字符串,我們使用Encoding.UTF8.GetString(buffer)。但同樣,UTF8 是一種可變長度編碼,我們不知道 UTF16 的長度。UTF8 也是如此。GetString我們需要計算長度為 UTF16 以將其轉換為字符串,因此我們在內部掃描字符串兩次。在偽代碼中,它是這樣的:

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

典型序列化程序的字符串格式為 UTF8,它不能解碼為 UTF16,因此即使您想要長度為 UTF16 以便作為 C# 字符串進行高效解碼,它也不在數據中。

但是,MemoryPack 在標頭中記錄 UTF16 長度和 UTF8 長度。因此,String.Create<TState>(Int32, TState, SpanAction<Char,TState>)Utf8.ToUtf16的組合為 C# String 提供了最有效的解碼。

關于有效負載大小

與可變長度編碼相比,整數的固定長度編碼的大小可能會膨脹。然而,在現代,使用可變長度編碼只是為了減小整數的小尺寸是一個缺點。

由于數據不僅僅是整數,如果真的想減小大小,應該考慮壓縮(LZ4[18],ZStandard[19],Brotli[20]等),如果壓縮數據,可變長度編碼幾乎沒有意義。如果你想更專業和更小,面向列的壓縮會給你更大的結果(例如,Apache Parquet[21])。為了與 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;?}
}

BitPackFormatter表示 bool[],bool 通常為 1 個字節,但由于它被視為 1 位,因此在一個字節中存儲八個布爾值。因此,序列化后的大小為 1/8。BrotliFormatter直接應用壓縮算法。這實際上比壓縮整個文件的性能更好。

10fdacdf3c7f74a6cea122c0b7945bba.png

這是因為不需要中間副本,壓縮過程可以直接應用于序列化數據。Uber 工程博客上的使用CLP 將日志記錄成本降低兩個數量級[22]一文中詳細介紹了通過根據數據以自定義方式應用處理而不是簡單的整體壓縮來提取性能和壓縮率的方法。

使用 .NET7 和 C#11 新功能

MemoryPack 在 .NET Standard 2.1 的實現和 .NET 7 的實現中具有略有不同的方法簽名。.NET 7 是一種更積極、面向性能的實現,它利用了最新的語言功能。

首先,序列化程序接口利用靜態抽象成員,如下所示:

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

MemoryPack 采用源生成器,并要求目標類型為[MemoryPackable]public partial class Foo,因此最終的目標類型為

[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?directlyT.Serialize(ref?this,?ref?Unsafe.AsRef(value));
}//
public?void?WriteValue<T>(scoped?in?T??value)
{//?call?Serialize?from?interface?virtual?methodIMemoryPackFormatter<T>?formatter?=?MemoryPackFormatterProvider.GetFormatter<T>();formatter.Serialize(ref?this,?ref?Unsafe.AsRef(value));
}

MemoryPackWriter/MemoryPackReader使用 ref字段。

public?ref?struct?MemoryPackWriter<TBufferWriter>where?TBufferWriter?:?IBufferWriter<byte>
{ref?TBufferWriter?bufferWriter;ref?byte?bufferReference;int?bufferLength;

換句話說,ref byte bufferReferenceint bufferLength的組合是Span<byte>的內聯。此外,通過接受 TBufferWriter 作為 ref TBufferWriter,現在可以安全地接受和調用可變結構 TBufferWriter:IBufferWrite<byte>

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

這兩個代碼看起來相同,但執行完全不同:foreach to IEnumerable<T> 檢索IEnumerator<T>,而 foreach to List<T>檢索結構List<T>.Enumerator,y 一個優化的專用結構。

但是,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);
}

來自 .NET 5 的 CollectionsMarshal.AsSpan 是枚舉 List<T> 的最佳方式。此外,如果可以獲得 Span<T>,則只能在 List<int>List<Vector3>的情況下通過復制來處理。

在反序列化的情況下,也有一些有趣的優化。首先,MemoryPack 的反序列化接受引用 T?值,如果值為 null,則如果傳遞該值,它將覆蓋內部生成的對象(就像普通序列化程序一樣)。這允許在反序列化期間零分配新對象創建。在List<T> 的情況下,也可以通過調用 Clear() 來重用集合。

然后,通過進行特殊的 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]);}}
}

EnsurceCapacity(capacity),可以預先擴展保存 List<T> 的內部數組的大小。這避免了每次都需要內部放大/復制。

但是 CollectionsMarshal.AsSpan,您將獲得長度為 0 的 Span,因為內部大小不會更改。如果我們有 CollectionMarshals.AsMemory,我們可以使用 MemoryMarshal.TryGetArray 組合從那里獲取原始數組,但不幸的是,沒有辦法從 Span 獲取原始數組。因此,我強制類型結構與 Unsafe.As 匹配并更改List<T>._size,我能夠獲得擴展的內部數組。

這樣,我們可以以僅復制的方式優化非托管類型,并避免 List<T>.Add(每次檢查數組大小),并通過Span<T>[index] 打包值,這比傳統序列化、反序列化程序性能要高得多。

雖然對List<T>的優化具有代表性,但要介紹的還有太多其他類型,所有類型都經過仔細審查,并且對每種類型都應用了最佳優化。

Serialize 接受 IBufferWriter<byte> 作為其本機結構,反序列化接受 ReadOnlySpan<byte>ReadOnlySequence<byte>

這是因為System.IO.Pipelines[23] 需要這些類型。換句話說,由于它是 ASP .NET Core 的服務器 (Kestrel) 的基礎,因此通過直接連接到它,您可以期待更高性能的序列化。

IBufferWriter<byte> 特別重要,因為它可以直接寫入緩沖區,從而在序列化過程中實現零拷貝。對 IBufferWriter<byte> 的支持是現代序列化程序的先決條件,因為它提供比使用 byte[] 或 Stream 更高的性能。開頭圖表中的序列化程序(System.Text.Json,protobuf-net,Microsoft.Orleans.Serialization,MessagePack for C#和 MemoryPack)支持它。

MessagePack 與 MemoryPack

MessagePack for C# 非常易于使用,并且具有出色的性能。特別是,以下幾點比 MemoryPack 更好

  • 出色的跨語言兼容性

  • JSON 兼容性(尤其是字符串鍵)和人類可讀性

  • 默認完美版本容錯

  • 對象和匿名類型的序列化

  • 動態反序列化

  • 嵌入式 LZ4 壓縮

  • 久經考驗的穩定性

MemoryPack 默認為有限版本容錯,完整版容錯選項的性能略低。此外,因為它是原始格式,所以唯一支持的其他語言是 TypeScript。此外,二進制文件本身不會告訴它是什么數據,因為它需要 C# 架構。

但是,它在以下方面優于 MessagePack。

  • 性能,尤其是對于非托管類型數組

  • 易于使用的 AOT 支持

  • 擴展多態性(聯合)構造方法

  • 支持循環引用

  • 覆蓋反序列化

  • 打字稿代碼生成

  • 靈活的基于屬性的自定義格式化程序

在我個人看來,如果你在只有 C#的環境中,我會選擇 MemoryPack。但是,有限版本容錯有其怪癖,應該事先理解它。MessagePack for C# 仍然是一個不錯的選擇,因為它簡單易用。

MemoryPack 不是一個只關注性能的實驗性序列化程序,而且還旨在成為一個實用的序列化程序。為此,我還以 MessagePack for C# 的經驗為基礎,提供了許多功能。

  • 支持現代 I/O API(IBufferWriter<byte>ReadOnlySpan<byte>ReadOnlySequence<byte>

  • 基于本機 AOT 友好的源生成器的代碼生成,沒有動態代碼生成(IL.Emit)

  • 無反射非泛型 API

  • 反序列化到現有實例

  • 多態性(聯合)序列化

  • 有限的版本容限(快速/默認)和完整的版本容錯支持

  • 循環引用序列化

  • 基于管道寫入器/讀取器的流式序列化

  • TypeScript 代碼生成和核心格式化程序 ASP.NET

  • Unity(2021.3) 通過 .NET 源生成器支持 IL2CPP

我們計劃進一步擴展可用功能的范圍,例如對MasterMemory 的 MemoryPack[24]支持和對 MagicOnion[25]的序列化程序更改支持等。我們將自己定位為Cysharp C# 庫[26]生態系統的核心。我們將付出很多努力來種下這一棵樹,所以對于初學者來說,請嘗試一下我們的庫!

版權信息

已獲得原作者獨家授權

原文版權:neuecc

翻譯版權:InCerry

原文鏈接:https://neuecc.medium.com/how-to-make-the-fastest-net-serializer-with-net-7-c-11-case-of-memorypack-ad28c0366516

參考資料

[1]

MemoryPack: https://github.com/Cysharp/MemoryPack

[2]

MessagePack for C#: https://github.com/neuecc/MessagePack-CSharp/

[3]

Utf8Json: https://github.com/neuecc/Utf8Json

[4]

SignalR MessagePack Hub: https://learn.microsoft.com/en-us/aspnet/core/signalr/messagepackhubprotocol

[5]

ZeroFormatter: https://github.com/neuecc/ZeroFormatter

[6]

Utf8Json: https://github.com/neuecc/Utf8Json

[7]

MagicOnion: https://github.com/Cysharp/MagicOnion

[8]

MasterMemory: https://github.com/Cysharp/MasterMemory

[9]

AlterNats: https://github.com/Cysharp/AlterNats

[10]

增量源生成器: https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.md

[11]

舊的源生成器: https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview

[12]

bincode: https://github.com/bincode-org/bincode

[13]

FlatBuffers: https://github.com/google/flatbuffers

[14]

Protocol-buffers數字類型: https://developers.google.com/protocol-buffers/docs/encoding#varints

[15]

MessagePack: https://github.com/msgpack/msgpack/blob/master/spec.md

[16]

CBOR: https://cbor.io/

[17]

結構沒有引用類型(非托管類型): https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/unmanaged-types

[18]

LZ4: https://github.com/lz4/lz4

[19]

ZStandard: http://facebook.github.io/zstd/

[20]

Brotli: https://github.com/google/brotli

[21]

Apache Parquet: https://parquet.apache.org/

[22]

CLP 將日志記錄成本降低兩個數量級: https://www.uber.com/en-DE/blog/reducing-logging-cost-by-two-orders-of-magnitude-using-clp/

[23]

System.IO.Pipelines: https://learn.microsoft.com/en-us/dotnet/standard/io/pipelines

[24]

MasterMemory的MemoryPack: https://github.com/Cysharp/MasterMemory

[25]

對MagicOnion: https://github.com/Cysharp/MagicOnion

[26]

Cysharp C# 庫: https://github.com/Cysharp/

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/280680.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/280680.shtml
英文地址,請注明出處:http://en.pswp.cn/news/280680.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

永不丟失照片:防彈照片備份的完整指南

There’s nothing as precious and irreplaceable as your personal photos and, with a little forethought and planning, there’s no reason to ever feel the heartbreak of losing even a single one of them to theft, broken devices, or disaster. 沒有比您的個人照片…

C# 檢查當前系統已安裝的程序app/兩種方法檢測

一、目的、構思 1.檢測當前系統有沒有安裝某個程序&#xff0c;如果沒有就重新安裝。 2.在網上找到了兩種方法&#xff0c;可惜都找不到需要檢測的app。 二、code實現 1.查找注冊列表方式。要在winform的project使用&#xff0c;在console project 貌似找不到Microsoft.Win3…

Integer源碼解析

版權聲明&#xff1a;本文為博主原創文章&#xff0c;未經博主允許不得轉載。 https://blog.csdn.net/wangyangzhizhou/article/details/77196626 概況 Java的Integer類主要的作用就是對基本類型int進行封裝&#xff0c;提供了一些處理int類型的方法&#xff0c;比如int到Strin…

MySQL InnoDB存儲引擎

呵呵噠。。。 MySQL體系結構和存儲引擎 首先要搞懂的是什么是數據庫&#xff0c;什么是數據庫實例。 數據庫&#xff1a;物理操作系統文件或其他形式文件類型的集合。 實例&#xff1a;MySQL數據庫由后臺線程以及一個共享內存區組成&#xff0c;實例才是真正對數據庫進行操作的…

Blazor學習之旅 (8) MudBlazor組件庫介紹

【Blazor】| 總結/Edison Zhou大家好&#xff0c;我是Edison。為了實現一個Web應用系統&#xff0c;需要有個看起來不丑的UI&#xff0c;而對于.NET程序員來說要做全棧開發還是有點難&#xff0c;而本篇介紹的這個UI組件庫正好可以幫助我們解決這個問題&#xff01;MudBlaozr是…

棉花糖多少錢_如何在6.0棉花糖及更高版本中訪問Android的正在運行的應用程序列表...

棉花糖多少錢In Android 5.x and below, accessing your list of running apps was simple—you’d jump into Settings > Apps > Running. Easy! In Android 6.0, however, Google moved this setting. It’s still not super difficult to find, but it’s a little tr…

IE不能直接顯示PDF的原因分析和解決方法

>>>>>問題<<<<<因為有系統用iframe顯示PDF&#xff0c;但PDF有時卻并不能順利地在流覽器中顯示&#xff0c;而是跳出下載對話框&#xff0c;要求下載&#xff0c;給user帶來很多困擾&#xff0c;也給我們系統維護人員帶來了麻煩&#xff0c;用了…

C# 程序圖標設置/winform 圖標

一、目的、實際情況 1.編寫一個winform 程序&#xff0c;發現有一個圖標非常有意義。區分其他程序&#xff0c;以及感覺在做產品而不是寫代碼。 2.添加圖標圖片發現&#xff0c;需要用ico格式。在線轉換&#xff08;某度搜索&#xff09;還是不靠譜。要微信登陸&#xff0c;登…

數字化轉型,究竟在“轉”什么?

這是頭哥侃碼的第265篇原創「頭哥嘮B嘮」這個欄目已經持續了幾個月了&#xff0c;沒想到還在繼續進行&#xff0c;并收獲了很多朋友們的喜愛。非常感謝大家的支持&#xff01;在上次的直播中&#xff0c;我找來了我的老熟人們。一個是右軍老師&#xff0c;之前 APISIX 的很多內…

C++ Primer 第Ⅲ部分筆記——類設計者的工具

1.對象移動 1.1右值引用 右值引用區別于普通引用&#xff0c;用兩個&表示 返回左值引用的函數&#xff0c;連同賦值、下標、解引用和前置遞增遞減運算符返回左值 返回非引用的函數&#xff0c;連同算術、關系、位以及后置遞增遞減運算符都生成右值 我們不能將左值引用綁定到…

Crash 的文明世界

題目描述 給一棵樹&#xff0c;求以每個點為根時下列式子的值。 題解 當k1時這就是一個經典的換根dp問題。 所以這道題還是要用換根dp解決。 部分分做法&#xff1a; 考慮轉移時是這樣的一個形式(圖是抄的)。 用二項式定理展開就可以nk2做了。 觀察到結果是一個xk的形式。 然后…

wampServer配置WWW根目錄遇到的坑

直接在官網下載之后開始安裝&#xff0c;一切正常 打開使用&#xff0c;一切正常 設置WWW目錄。坑了一波 按照的都是百度上的教程&#xff0c;設置httpd.conf 這里配置之后網頁訪問127.0.0.1 還是localhost都還是原始的www目錄 后來 我發現了這里 是配置虛擬URL的地方。以上是正…

windows安裝程序創建_如何在Windows上創建已安裝程序的列表

windows安裝程序創建Reinstalling Windows is a good way to fix serious problems with your computer, or just to get a fresh slate. But before you reinstall Windows, you should make a list of programs you currently have installed on your PC so you know what yo…

實現一個更新所有 dotnet tool 的 dotnet tool

實現一個更新所有 dotnet tool 的 dotnet toolIntrodotnet tool 是從 .NET Core 2.1 開始支持的命令行工具&#xff0c;在使用 dotnet tool 比較多了的時候&#xff0c;想要更新所有的 dotnet tool 就比較麻煩&#xff0c;而目前 .NET SDK 還不支持&#xff0c;也有一些人希望能…

C# 普通權限運行程序\非管理員運行\降低權限運行

一、目的與實際 1.VS設置管理員權限運行程序后&#xff0c;發現調用powershell命令或程序需要普通權限即可&#xff0c;Administrator權限反而錯。 2.網上搜索關鍵字&#xff0c;大部分都是怎么使用管理員權限運行。 3.bing搜索意外發現有相關資料&#xff0c;記錄分享。 二…

am335x PDK3.0 設置為單網口配置記錄

原來的配置是雙網口的&#xff0c;現在要配置為單網口。一直以為這個配置是在 make menuconfig 里面&#xff0c; 沒想到是在設備樹里面。修改設備樹// vim arch/arm/boot/dts/am335x-sbc7109.dts722 &mac {723 pinctrl-names "default", "sleep"…

[AHOI2009]飛行棋 BZOJ1800

題目描述 給出圓周上的若干個點&#xff0c;已知點與點之間的弧長&#xff0c;其值均為正整數&#xff0c;并依圓周順序排列。 請找出這些點中有沒有可以圍成矩形的&#xff0c;并希望在最短時間內找出所有不重復矩形。 輸入輸出格式 輸入格式&#xff1a;第一行為正整數N&…

webapi+Quartz.NET解決若干定時程序同時運行的問題

項目現狀&#xff1a; 有若干定時程序需要自啟動運行&#xff0c;為了簡便程序部署等問題&#xff0c;采取這種辦法把定時程序集中管理到webapi中跟隨api發布 代碼架構介紹&#xff1a; 新建一個類庫&#xff0c;類庫引用Quartz&#xff08;Quartz.2.3.2&#xff09;&#xff0…

mac恢復iphone_免費下載:舊Mac和iPhone壁紙的令人震驚的完整檔案

mac恢復iphoneLove or hate Apple, you’ve got to admit: their background images are consistently stunning. Now you can download all of them. 愛或恨蘋果&#xff0c;您必須承認&#xff1a;它們的背景圖像始終令人贊嘆。 現在&#xff0c;您可以下載所有這些文件。 A …

Django01-1: request 方法

#POST request.method #返回全大寫字符穿&#xff0c;<class str> POST/GETrequest.POST #用戶提交數據&#xff0c;不包含文件 #<QueryDict>request.POST.get(hobby) #拿列表最后一個 request.POST.getList(hobby) #拿多個&#xff0c;列表全部#GET 獲取url &a…