前阵子看到MemoryPack序列化库,加上要做帧同步校验/回滚,决定照着抄了一个精简版序列化工具。基于IBufferWritere<byte>ReadOnlySpan<byte>ReadOnlySequence<byte>(未实现,比较复杂)等现代 .NET IO Api,并配合SourceGenerator代码生成,实现高效的零反射序列化,并且无缝兼容unity aot环境。

特性

  1. 支持unmanaged、class、array、list多种类型
  2. 引用序列化,引用类型(容器除外),全部基于引用来序列化,适应复杂oop逻辑
  3. 容器类支持,List、HashSet、Dictionary等
  4. 多态序列化,通过编写简单代码,可以轻松支持Interface/Abstract Class,并且基类、子类可跨程序集
  5. 多种序列化事件,可以穿插自定义逻辑
  6. 可dump字符串,方便debug

性能

测试结果

|               Method | BufferCount |            Mean | Error | Allocated |
|--------------------- |------------ |----------------:|------:|----------:|
| MemoryPack_Serialize |          10 |        63.75 ns |    NA |         - |
|  FSnapshot_Serialize |          10 |        32.96 ns |    NA |         - |
|     Normal_Serialize |          10 |        56.73 ns |    NA |         - |
| MemoryPack_Serialize |         100 |        91.63 ns |    NA |         - |
|  FSnapshot_Serialize |         100 |        61.32 ns |    NA |         - |
|     Normal_Serialize |         100 |       542.56 ns |    NA |         - |
| MemoryPack_Serialize |        1000 |       349.83 ns |    NA |         - |
|  FSnapshot_Serialize |        1000 |       320.90 ns |    NA |         - |
|     Normal_Serialize |        1000 |     5,260.09 ns |    NA |         - |
| MemoryPack_Serialize |       10000 |     3,916.05 ns |    NA |         - |
|  FSnapshot_Serialize |       10000 |     3,853.95 ns |    NA |         - |
|     Normal_Serialize |       10000 |    52,770.45 ns |    NA |         - |
| MemoryPack_Serialize |       65536 |    30,041.41 ns |    NA |         - |
|  FSnapshot_Serialize |       65536 |    29,814.07 ns |    NA |         - |
|     Normal_Serialize |       65536 |   347,519.04 ns |    NA |         - |
| MemoryPack_Serialize |      100000 |    44,204.24 ns |    NA |         - |
|  FSnapshot_Serialize |      100000 |    45,657.86 ns |    NA |       1 B |
|     Normal_Serialize |      100000 |   535,551.86 ns |    NA |       1 B |
| MemoryPack_Serialize |     1000000 | 1,479,738.87 ns |    NA |       6 B |
|  FSnapshot_Serialize |     1000000 | 1,379,992.19 ns |    NA |       1 B |
|     Normal_Serialize |     1000000 | 5,692,993.75 ns |    NA |       5 B |

测试代码

[Benchmark]
public void MemoryPack_Serialize()
{
    MemoryPackSerializer.Serialize(bufferWriter, array_1);

    bufferWriter.Clear();
}

[Benchmark]
public void FSnapshot_Serialize()
{
    using var writer = new FSnapshotWriter(bufferWriter);
    writer.WriteUnmanagedArray(array_1);

    bufferWriter.Clear();
}

[Benchmark]
public void Normal_Serialize()
{
    var size = Unsafe.SizeOf<Vector3>();
    for (int i = 0; i < array_1.Length; i++)
    {
        var v = array_1[i];
        var span = bufferWriter.GetSpan(size);
        ref var dst = ref MemoryMarshal.GetReference(span);
        Unsafe.WriteUnaligned(ref dst, v.X);
        Unsafe.WriteUnaligned(ref Unsafe.Add(ref dst, Unsafe.SizeOf<float>()), v.Y);
        Unsafe.WriteUnaligned(ref Unsafe.Add(ref dst, 2 * Unsafe.SizeOf<float>()), v.Z);
        bufferWriter.Advance(size);
    }

    bufferWriter.Clear();
}

例子

序列化类

using System;
using System.Collections.Generic;
using Saro.FSnapshot;

namespace App;

[FSnapshotUnionFormatter(typeof(Animal))]
[FSnapshotUnion(0, typeof(Dog))]
[FSnapshotUnion(1, typeof(Rabbit))]
public partial class AnimalUnionFormatter
{ }

[FSnapshotable]
[Serializable]
public partial class Animals
{
    public enum EType
    {
        Type0 = 0,
        Type1 = 1,
    }

    [FSnapshot] public EType type = EType.Type1;
    [FSnapshot] public List<Animal> animals_list;
    [FSnapshot] public Animal[] animals_array;
}


[FSnapshotable]
[Serializable]
public abstract partial class Animal
{
    [FSnapshot] public string baseText;
}

[FSnapshotable]
[Serializable]
public partial class Dog : Animal
{
    [FSnapshot] public string dog;

    [FSnapshot] public List<int> int_list;
    [FSnapshot] public int[] int_array;

    [FSnapshot] public int? int_nullable; // 8b
    [FSnapshot] public Nullable<byte> int_nullable_null; // 2b
}

[FSnapshotable]
[Serializable]
public partial class Rabbit : Animal
{
    [FSnapshot] public string rabbit;

    [FSnapshot] public List<(int, bool)> tuple_list;
    [FSnapshot] public HashSet<int> set;
    [FSnapshot] public Dictionary<int, long> dic;
    [FSnapshot] public Dictionary<string, string> dic_obj;

    [FSnapshotOnSerializing]
    void OnSerializing(ref FSnapshotWriter writer)
    {
        writer.WriteString("header");
    }
    [FSnapshotOnSerialized] void OnSerialized() { }
    [FSnapshotOnDeserializing]
    void OnDeserializing(ref FSnapshotReader reader)
    {
        var header = reader.ReadString();
    }
    [FSnapshotOnDeserialized] void OnDeserialized() { }
}

使用方式

using System;
using System.Buffers;
using System.Collections.Generic;
using Saro.FSnapshot;
using App;
using System.Text;

Animals CreateObj()
{
    var dog = new Dog
    {
        baseText = "animal",
        dog = "dog",
        int_array = new int[] { 1, 2, 3 },
        int_list = new List<int> { 11, 22, 33 },
        int_nullable = 999,
    };

    var rabbit = new Rabbit
    {
        baseText = "animal",
        rabbit = "rabbit",
        tuple_list = new List<(int, bool)> { (1, true), (2, false) },
        set = new HashSet<int>() { 3, 2, 1 },
        dic = new Dictionary<int, long> { { 1, 1 }, { 2, 2 }, { 3, 3 } },
        dic_obj = new Dictionary<string, string> { { "a", "a" }, { "b", "b" }, { "c", "c" } },
    };

    var animals = new Animals();
    animals.animals_array = new Animal[]
    {
            dog,
            rabbit,
    };
    animals.animals_list = new List<Animal>(animals.animals_array);
    animals.animals_list.Reverse();

    return animals;
}

void Test2()
{
    var bufferWriter = new ArrayBufferWriter<byte>(2048);

    // create obj
    var animals = CreateObj();

    // take snapshot
    {
        using var writer = new FSnapshotWriter(bufferWriter);
        writer.WriteObject(animals);

        DumpJson("animals:", animals);
    }

    // resotre snapshot
    {
        Animals newAnimals = null;

        using var reader = new FSnapshotReader(bufferWriter.WrittenSpan);
        reader.ReadObject(ref newAnimals);

        DumpJson("newAnimals:", newAnimals);

        DumpText("newAnimals:", newAnimals);

        // DumpRaw("newAnimals:", newAnimals);
    }
}

Test2();

/*

OUTPUT

animals:
{"type":1,"animals_list":[{"$type":"App.Rabbit, ConsoleApp","rabbit":"rabbit","tuple_list":[{"Item1":1,"Item2":true},{"Item1":2,"Item2":false}],"set":[3,2,1],"dic":{"1":1,"2":2,"3":3},"dic_obj":{"a":"a","b":"b","c":"c"},"baseText":"animal"},{"$type":"App.Dog, ConsoleApp","dog":"dog","int_list":[11,22,33],"int_array":[1,2,3],"int_nullable":999,"int_nullable_null":null,"baseText":"animal"}],"animals_array":[{"$type":"App.Dog, ConsoleApp","dog":"dog","int_list":[11,22,33],"int_array":[1,2,3],"int_nullable":999,"int_nullable_null":null,"baseText":"animal"},{"$type":"App.Rabbit, ConsoleApp","rabbit":"rabbit","tuple_list":[{"Item1":1,"Item2":true},{"Item1":2,"Item2":false}],"set":[3,2,1],"dic":{"1":1,"2":2,"3":3},"dic_obj":{"a":"a","b":"b","c":"c"},"baseText":"animal"}]}

newAnimals:
{"type":1,"animals_list":[{"$type":"App.Rabbit, ConsoleApp","rabbit":"rabbit","tuple_list":[{"Item1":1,"Item2":true},{"Item1":2,"Item2":false}],"set":[3,2,1],"dic":{"1":1,"2":2,"3":3},"dic_obj":{"a":"a","b":"b","c":"c"},"baseText":"animal"},{"$type":"App.Dog, ConsoleApp","dog":"dog","int_list":[11,22,33],"int_array":[1,2,3],"int_nullable":999,"int_nullable_null":null,"baseText":"animal"}],"animals_array":[{"$type":"App.Dog, ConsoleApp","dog":"dog","int_list":[11,22,33],"int_array":[1,2,3],"int_nullable":999,"int_nullable_null":null,"baseText":"animal"},{"$type":"App.Rabbit, ConsoleApp","rabbit":"rabbit","tuple_list":[{"Item1":1,"Item2":true},{"Item1":2,"Item2":false}],"set":[3,2,1],"dic":{"1":1,"2":2,"3":3},"dic_obj":{"a":"a","b":"b","c":"c"},"baseText":"animal"}]}

newAnimals:
$type:Animals
  $src:0
  Type1
  [
    $union:1
    $type:Rabbit
      $src:1
      animal
      rabbit
      [(1, True),(2, False)]
      [
        3
        2
        1
      ]
      {
        [1, 1]
        [2, 2]
        [3, 3]
      }
      {
        [
          a
          ,
          a
        ]
        [
          b
          ,
          b
        ]
        [
          c
          ,
          c
        ]
      }
    $union:0
    $type:Dog
      $src:2
      animal
      dog
      [11,22,33]
      [1,2,3]
      999
      null
  ]
  [
    $ref:2
    $ref:1
  ]

*/