测试环境: Windows 11 (25H2) | Intel Core i7-13650HX @ 2.60GHz | .NET 10.0
二进制序列化性能对比
| 测试场景 | LuminPack | MemoryPack | MessagePack | 性能优势 |
|---|---|---|---|---|
| 简单对象序列化 | 80.27 ns | 294.04 ns | 1,471.37 ns | 3.6x ~ 18.3x ⚡ |
| 简单对象反序列化 | 263.86 ns | 355.66 ns | 3,189.42 ns | 1.3x ~ 12.1x ⚡ |
| 多态对象序列化 | 4.638 μs | 15.591 μs | 77.121 μs | 3.4x ~ 16.6x ⚡ |
| 多态对象反序列化 | 10.008 μs | 17.099 μs | 114.295 μs | 1.7x ~ 11.4x ⚡ |
JSON 序列化性能对比
| 测试场景 | LuminPack | System.Text.Json | Newtonsoft.Json | 性能优势 |
|---|---|---|---|---|
| JSON 序列化 | 18.12 μs | 22.63 μs | 47.11 μs | 1.2x ~ 2.6x 🚀 |
| JSON 反序列化 | 16.47 μs | 25.82 μs | 62.43 μs | 1.6x ~ 3.8x 🚀 |
LuminPack是一款高性能序列化库,对于嵌套类,LuminPack比Memorypack,Messagepack快50%,200%。对于泛型类,LuminPack比Memorypack,Messagepack快400%。对于一些特殊集合,LuminPack甚至快上22倍。
LuminPack最初是专门为Unity开发的,为Unity在存档,网络等高性能场景下提供接近手写特定类解析器的性能,LuminPack性能如何快的原因也是如此。对于LuminPack的基础类型,LuminPack会直接解析写入解析器,与此同时,LuminPack大量学习并借鉴了MemoryPack对于特定于C#的内存操纵,并通过指针反射,内存映射实现更高性能。
本人没有写Readme的经验,因此本文借鉴了Memorypack的Readme写法。
除了性能,LuminPack包含MemoryPack绝大部分特性,对于仅包含LuminPack基础类型的数据,二者甚至可以互相序列化(好像不行了,没测试)。
- 支持现代I / O api (
ReadOnlySpan<byte>,ReadOnlySequence<byte>) - 更新支持至.Net10
- 基于增量源代码生成器的代码生成
- 同时支持二进制、Json
- 基于非托管内存的WriterBuffer
- 无反射
- 多态序列化
- 有限的版本容忍(快速/默认)和完全的版本容忍支持
- 循环引用序列化
- AOT友好
- 反序列化缓存池
- 通过增量源代码生成器支持Unity
- Nuget搜索LuminPack安装
- 自行导入仓库的dll文件
- Unity编辑器扩展,可视化生成文件
- 内置压缩功能
- 内置加密功能
定义要序列化的结构体或类,并用 [LuminPackable] 属性对其进行注释。
using LuminPack;
[LuminPackable]
public class Person
{
public int Age { get; set; }
public string Name { get; set; }
}序列化代码将由c#源代码生成器功能生成,您可以查询生成的名为'命名空间+外层类名(如有)+ClassNameParser.g.cs'的文件查看详细代码。
调用 LuminPackSerializer.Serialize<T>/Deserialize<T> 序列化/反序列化二进制对象实例。
调用 LuminPackSerializer.SerializeJson<T>/DeserializeJson<T> 序列化/反序列化Json对象实例。
var item = new Person { Age = 18, Name = "Light" };
var buffer = LuminPackPackSerializer.Serialize(item);
var bufferJson = LuminPackPackSerializer.SerializeJson(item);
var result = LuminPackSerializer.Deserialize<Person>(buffer);
var resultJson = LuminPackSerializer.DeserializeJson<Person>(buffer);默认情况下,LuminPack基础类型将实现最高性能
Int, UInt, Byte, Short, UShort, Long, ULong,
Float, Double, Char, String, Decimal, Bool,
Enum, Struct, Class, List, Array
默认情况下,这些类型可以被序列化:
- .Net所有非托管类型 (
byte,int,bool,char,double, etc.) string,decimal,Half,Int128,UInt128,Guid,Rune,BigIntegerTimeSpan,DateTime,DateTimeOffset,TimeOnly,DateOnly,TimeZoneInfoComplex,Plane,QuaternionMatrix3x2,Matrix4x4,Vector2,Vector3,Vector4Uri,Version,StringBuilder,Type,BitArray,CultureInfoT[],T[,],T[,,],T[,,,],Memory<>,ReadOnlyMemory<>,ArraySegment<>,ReadOnlySequence<>Nullable<>,Lazy<>,KeyValuePair<,>,Tuple<,...>,ValueTuple<,...>List<>,LinkedList<>,Queue<>,Stack<>,HashSet<>,SortedSet<>,PriorityQueue<,>Dictionary<,>,SortedList<,>,SortedDictionary<,>,ReadOnlyDictionary<,>Collection<>,ReadOnlyCollection<>,ObservableCollection<>,ReadOnlyObservableCollection<>,ReadOnlyCollectionBuilder<>IEnumerable<>,ICollection<>,IList<>,IReadOnlyCollection<>,IReadOnlyList<>,ISet<>IDictionary<,>,IReadOnlyDictionary<,>,ILookup<,>,IGrouping<,>,ConcurrentBag<>,ConcurrentQueue<>,ConcurrentStack<>,ConcurrentDictionary<,>,BlockingCollection<>- Immutable collections (
ImmutableList<>, etc.) and interfaces (IImmutableList<>, etc.)
[LuminPackable] 可以注释到任何 class , abstract class , struct , record , record struct 和 interface 。如果类型 struct 或 record struct 不包含引用类型(c#非托管类型),则不使用任何直接从内存序列化/反序列化的规则,LuminPack会直接复制内存。
默认情况下, [LuminPackable] 序列化公共实例属性或字段。可以使用 [LuminPackIgnore] 删除序列化目标, [LuminPackInclude] 将私有成员提升为序列化目标。
[LuminPackable]
public class MyClass
{
[LuminPackInclude]
private MyClass2 myClass;
}
[LuminPackable]
public class MyClass2
{
[LuminPackInclude]
private int num1;
[LuminPackIgnore]
public long num2;
public short num3;
public double num4;
public float num5;
public string[] strings;
}LuminPack有39条诊断规则( LuminPack001 到LuminPack039 )
LuminPack不序列化成员名或其他信息,而是按照声明的顺序序列化字段。如果类型是继承的,则按照父级→子级的内存布局顺序执行序列化。成员的顺序不能因反序列化而改变。关于模式演变,请参阅版本容忍部分。
默认的序列化顺序和布局是按照声明顺序的,如果想要更改,您可以使用 [LuminPackOrder()]
注:对于循环引用和完全版本容忍模式,每个字段和属性必须注释[LuminPackOrder()]
// serialize Prop0 -> Prop1
[LuminPackable]
public class MyClass
{
[LuminPackOrder(1)]
public int Prop1 { get; set; }
[LuminPackOrder(0)]
public int Prop0 { get; set; }
}LuminPack不依赖构造函数反序列化,因此您可以随意定义构造函数。
LuminPack默认支持 0 ~ 249 个成员字段
[LuminPackableObject]可以作用于任何字段以及属性,这将告诉LuminPackCodeGenerator不要直接解析该字段并写入Myclass的解析器,而是通过注册在LuminPack的Myclass2的解析器去解析。通常情况下,这会损失大概30%的性能,因此如果您遇到源代码生成器生成错误代码等情况,可以尝试用[LuminPackableObject]标记字段或属性。
以LuminPackable的示例代码为例。在 .Net8 以上的平台, 对于嵌套类私有字段的解析,注释[LuminPackInclude]将会 正常工作。但在 .Net Standard2.1 平台,这将不会工作。
例如以上示例代码,对于MyClass的 "private MyClass2 myClass;" 字段,使用[LuminPackInclude]将会正常工作并解析MyClass2的所有 public 字段,但是不会解析Myclass2的 private 字段, 即使您在Myclass2的 private 字段标记[LuminPackInclude]。如果想要正常工作,请使用[LuminPackableObject]来取消基础类型的解析。
[LuminPackable]
public class MyClass
{
[LuminPackInclude]
[LuminPackableObject] //这将使MyClass2的私有字段num1正常解析
private MyClass2 myClass;
}在实例方法或静态方法标记[LuminPackOnSerialized],[LuminPackOnSerializing],[LuminPackOnDeserialized],[LuminPackOnDeserializing]等特性
[LuminPackOnSerializing]
public static void OnSerializing()
{
Console.WriteLine("OnSerializing");
}
[LuminPackOnSerialized]
public void OnSerialized()
{
Console.WriteLine("OnSerialized");
}
[LuminPackOnDeserialized]
public void OnDeserialized()
{
Console.WriteLine("OnDeserialized");
}
[LuminPackOnDeserializing]
public static void OnDeserializing()
{
Console.WriteLine("OnDeserializing");
}LuminPack支持反序列化从缓存池取代new创建实例,减少GC开销。
LuminPack并不实现缓存池逻辑,需要用户自行实现,同时用户也应该注意Return实例,LuminPack并不追踪对象去Return
要启用缓存池,需要在静态方法上标记 [LuminPackPoolRent] 特性
[LuminPackPoolRent]
public static SimpleClass Rent()
{
return this.Pool.Rent();
}LuminPack支持序列化接口和抽象类对象,实现多态序列化。
LuminPack支持最低程度的自动收集继承类,对于标记了[LuminPackable]特性的abstract,interface,LuminPack会自动收集符合以下规则的子类
规则:
1. 如果子类泛型参数数量超过基类,不收集
2. 如果子类泛型参数不是直接传递给基类(如 MyClass<U> : MyClassBase<T>),不收集
3. 如果子类有约束且和基类约束不一样,不收集
4. 如果基类被完全具象化(如 MyClassBase<int>),子类不能有泛型参数
允许的情况:
- MyClass<T> : MyClassBase<T> (泛型参数完全匹配,约束一致)
- MyClass : MyClassBase<int> (完全具体化,子类无泛型)
Id分配规则:从0开始递个递增,如果遇到[LuminPackUnion]显示注册过的Id,则跳过。
对于不收集的类型,LuminPack支持手动注册。与MemoryPack的Union相同。只有接口和抽象类允许使用 [LuminPackUnion] 属性进行注释。需要唯一的联合标记。
// Annotate [LuminPackable] and inheritance types with [LuminPackUnion]
// Union also supports interface class
[LuminPackable]
[LuminPackUnion(0, typeof(Child1))]
[LuminPackUnion(1, typeof(Child2))]
public abstract class IUnionSample
{
}
[LuminPackable]
public class Child1 : IUnionSample
{
public int num;
}
[LuminPackable]
public class Child2 : IUnionSample
{
public string str;
}
IUnionSample data = new Child1() { num = 114514};
// Serialize
var buffer = LuminPackSerializer.Serialize(data);
// Deserialize
var result = LuminPackSerializer.Deserialize<IUnionSample>(buffer);
switch (result)
{
case Child1 x:
Console.WriteLine(x.num);
break;
case Child2 x:
Console.WriteLine(x.str);
break;
default:
break;
}对于LuminPackUnion的Tag,支持 0 ~ 65535, 对与250以下的性能更佳。因此推荐使用250以下的值作为Tag
LuminPack支持跨程序集多态序列化。如果程序集A定义了abstract类,程序集B的类继承了程序集A的abstract类,由于源生成器的限制,源生成器并不能分析到程序集B继承的子类,不会生成对应的序列化代码。此时需要用户手动注册,调用源生成器为A生成的abstract的Parser类的Register方法。以下是示例代码
global::LuminPack.Generated.LuminPackBenchmark_SimpleClassBaseParser.Register();
//所有生成的Parser都在LuminPack.Generated命名空间下,生成的Parser的类名规则为:命名空间+外层类名+类名+ParserRegister方法接受6个参数,分别是Type,Id,二进制序列化函数指针,二进制反序列化函数指针,Json序列化函数指针,Json反序列化函数指针
用户需要手写几个静态函数
private static unsafe void WriteLuminPackBenchmark_FooA(ref LuminPackWriter writer, ref global::LuminPackBenchmark.IFoo value)
{
writer.WriteUnionHeader(0);
writer.WritePolymorphismValue(LuminPackMarshal.As<global::LuminPackBenchmark.IFoo, global::LuminPackBenchmark.FooA>(ref value));
}
private static unsafe void ReadLuminPackBenchmark_FooA(ref LuminPackReader reader, ref global::LuminPackBenchmark.IFoo value)
{
global::LuminPackBenchmark.FooA tempValue = default!;
reader.ReadPolymorphismValue(ref tempValue);
value = LuminPackMarshal.As<global::LuminPackBenchmark.FooA, global::LuminPackBenchmark.IFoo>(ref tempValue!);
}
private static unsafe void WriteJsonLuminPackBenchmark_FooA(ref global::LuminPack.Core.LuminPackJsonWriter writer, ref global::LuminPackBenchmark.IFoo value)
{
writer.WriteObjectStart();
writer.WritePropertyName(LuminPackConstUtf8.TypeU8);
writer.WriteInt(0);
writer.WritePropertyName(LuminPackConstUtf8.ValueU8);
writer.WriteValue(ref LuminPackMarshal.As<global::LuminPackBenchmark.IFoo, global::LuminPackBenchmark.FooA>(ref value)!);
writer.WriteObjectEnd();
}
private static unsafe void ReadJsonLuminPackBenchmark_FooA(ref global::LuminPack.Core.LuminPackJsonReader reader, ref global::LuminPackBenchmark.IFoo value)
{
global::LuminPackBenchmark.FooA tempValue = default!;
reader.ReadValue(ref tempValue);
value = LuminPackMarshal.As<global::LuminPackBenchmark.FooA, global::LuminPackBenchmark.IFoo>(ref tempValue!);
}
// 最后调用Register方法
LuminPack.Generated.LuminPackBenchmark_IFooParser.Register(
typeof(FooA), 100,
&WriteLuminPackBenchmark_FooA,
&ReadLuminPackBenchmark_FooA,
&WriteJsonLuminPackBenchmark_FooA,
&ReadJsonLuminPackBenchmark_FooA);在默认情况下 LuminPack的代码生成模式( GenerateType.Object ), 仅支持有限的模式演化。
- 如果数据类型是非托管数据,例如Struct(不包含引用类型)。不能更改数据
- 可以添加成员,不能删除成员。
- 不能更改成员名称
- 不能更改成员顺序
- 不能更改成员类型
[LuminPackable]
public class MyClass
{
public int Prop1 { get; set; }
public long Prop2 { get; set; }
}
// Add is OK.
[LuminPackable]
public class MyClass
{
public int Prop1 { get; set; }
public long Prop2 { get; set; }
public int? AddedProp { get; set; }
}
// Remove is NG.
[LuminPackable]
public class MyClass
{
// public int Prop1 { get; set; }
public long Prop2 { get; set; }
}
// Change order is NG.
[LuminPackable]
public class MyClass
{
public long Prop2 { get; set; }
public int Prop1 { get; set; }
}当使用 GenerateType.VersionTolerant 时,它支持完全的版本容忍。
[LuminPackable(GenerateType.VersionTolerant)]
public class VersionTolerantObject1
{
[LuminPackOrder(0)]
public int MyProperty0 { get; set; } = default;
[LuminPackOrder(1)]
public long MyProperty1 { get; set; } = default;
[LuminPackOrder(2)]
public short MyProperty2 { get; set; } = default;
}
[LuminPackable(GenerateType.VersionTolerant)]
public class VersionTolerantObject2
{
[LuminPackOrder(0)]
public int MyProperty0 { get; set; } = default;
// deleted
//[LuminPackOrder(1)]
//public long MyProperty1 { get; set; } = default;
[LuminPackOrder(2)]
public short MyProperty2 { get; set; } = default;
// added
[LuminPackOrder(3)]
public short MyProperty3 { get; set; } = default;
}GenerateType.VersionTolerant 比 GenerateType.Object 性能更差,使用时请注意。
// to enable circular-reference, use GenerateType.CircularReference
[LuminPackable(GenerateType.CircularReference)]
public class Node
{
[LuminPackOrder(0)]
public Node? Parent { get; set; }
[LuminPackOrder(1)]
public Node[]? Children { get; set; }
}GenerateType.CircularReference 具有与版本容忍相同的特性。
对象引用跟踪只对标记为 GenerateType.CircularReference 的对象进行。如果要跟踪任何其他对象,请对其进行包装。
LuminPack的序列化池通过Marshal申请非托管内存,这极大提高了Buffer扩容的性能。
因此,请确保所有WriteBuffer调用Dispose方法,以释放非托管内存。
LuminPack内置了高性能ObjectPool
#if NET8_0_OR_GREATER
private static readonly ObjectPool<ReusableLinkedArrayBufferWriter> _pool =
new(MaxPoolSize);
#else
private static readonly ObjectPool<ReusableLinkedArrayBufferWriter> _pool =
new(new BufferWriterPolicy(), MaxPoolSize);
#endif
public static ReusableLinkedArrayBufferWriter Rent() => _pool.Rent();
public static void Return(ReusableLinkedArrayBufferWriter writer) => _pool.Return(writer);.Net8以上版本,ObjectPool的对象需要继承IPooledObjectPolicy接口。
.Net Standard2.1版本,则需要单独定义继承继承IPooledObjectPolicy接口的类,通过依赖注入的方式。
LuminPack对于Unity有特殊优化,以达到.Net8版本相同的性能。
- 对于
List<>,Stack<>,Queue<>,Collection<>,ReadonlyCollection<>,ObserveableCollection<>,ReadonlyObserveableCollection<>,ReadOnlyCollectionBuilder<>的非托管泛型,LuminPack比MemoryPack快22倍 (1024数据量) - Serialize API和Deserialize API的类型检查优化,提高处理特殊数据的性能。
端序必须 Little Endian 。
非托管结构是不包含引用类型的c#结构,类似于c#非托管类型的约束。序列化结构布局,包括填充。
(byte memberCount, [values...])
对象头文件中的成员计数为1字节无符号字节。成员数允许 0 到 249 , 255 表示对象 null 。值存储成员数的内存包值。
(byte memberCount, [varint byte-length-of-values...], [values...])
版本容忍对象与 Object 类似,但在头部包含值的字节长度。变长整数遵循以下规范:第一个有符号字节(sbyte)是值或类型代码,接下来的 X 个字节是具体值。其中,0 到 127 对应无符号字节值,-1 到 - 120 对应有符号字节值,-121 对应字节(byte),-122 对应有符号字节(sbyte),-123 对应无符号短整数(ushort),-124 对应短整数(short),-125 对应无符号整数(uint),-126 对应整数(int),-127 对应无符号长整数(ulong),-128 对应长整数(long)。
(byte memberCount, [varint byte-length-of-values...], varint referenceId, [values...])
(250, varint referenceId)
循环引用对象类似于版本容忍对象,但如果memberCount为250,则下一个变量(unsigned-int32)为referenceId。如果不是,则在字节长度值之后写入变量referenceId。
(int utf16-length, utf16-value)
(int ~utf8-byte-count, int utf16-length, utf8-bytes)
字符串有两种形式,UTF16和UTF8。如果第一个4byte有符号整数 -1 ,表示null。 0 ,表示空。UTF16与collection相同(序列化为 ReadOnlySpan<char> , UTF16 -value的字节数为UTF16 -length * 2)。如果第一个有符号整数<= -2 ,则value用UTF8编码。Utf8-byte-count以补码形式编码, ~utf8-byte-count 检索字节数。下一个有符号整数是utf16-length,它允许 -1 表示未知长度。Utf8-bytes存储utf8-byte-count的字节数。
(byte tag, value)
(250, ushort tag, value)
第一个无符号字节是用于区分值类型或标志的标记, 0 到 249 表示标记, 250 表示下一个无符号短标记, 255 表示 null 。
(int length, [values...])
集合头的数据计数为4字节有符号整数, -1 表示 null 。头字节存储数据长度。
(values...)
元组是固定大小的非空值集合。 KeyValuePair<TKey, TValue> 和 ValueTuple<T,...> 被序列化为Tuple。
This library is licensed under the MIT License.
