在 json.net 上使用 protobuf-net 时的高内存分配

High memory allocations when using protobuf-net over json.net

我最近的任务是探索 protobuf-net within a performance critical application. It currently uses Newtonsoft.Json 的使用,并且在大多数情况下,protobuf-net 已经显示出出色的性能提升。但在某些情况下,内存分配正在通过屋顶进行,我被困在如何弄清楚发生了什么。

我整理了一个小型控制台应用程序来重现该问题(该问题最初是通过性能回归测试发现的)。出于明显的原因,我无法 post 那个精确的测试,但我有一个类似的例子;

public class Program
{
    public static void Main(string[] args)
    {
        AppDomain.MonitoringIsEnabled = true;
        var useProtoBuf = args.Length > 0;

        if (useProtoBuf)
        {
            Console.WriteLine("Using protobuf-net");
        }
        else
        {
            Console.WriteLine("Using json.net");
        }

        var runtimeTypeModel = TypeModel.Create();
        runtimeTypeModel.Add(typeof(TestResult), true);
        var list = new List<Wrapper>();

        for (var index = 0; index < 1_000_000; index++)
        {
            list.Add(new Wrapper
            {
                Value = "C5CAD058-3A05-48EA-9626-A6B4F692B14E"
            });
        }

        var result = new TestResult
        {
            First = new CollectionWrapper
            {
                Collection = list
            }
        };

        for (var i = 0; i < 25; i++)
        {
            if (useProtoBuf)
            {
                using (var stream = File.Create(@"..\..\protobuf-net.bin"))
                {
                    runtimeTypeModel.Serialize(stream, result);
                }
            }
            else
            {
                using (var stream = File.CreateText(@"..\..\json.net.json"))
                using (var writer = new JsonTextWriter(stream))
                {
                    new JsonSerializer().Serialize(writer, result);
                }
            }
        }

        Console.WriteLine($"Took: {AppDomain.CurrentDomain.MonitoringTotalProcessorTime.TotalMilliseconds:#,###} ms");
        Console.WriteLine($"Allocated: {AppDomain.CurrentDomain.MonitoringTotalAllocatedMemorySize / 1024:#,#} kb");
        Console.WriteLine($"Peak Working Set: {Process.GetCurrentProcess().PeakWorkingSet64 / 1024:#,#} kb");
    }

    [ProtoContract]
    public class Wrapper
    {
        [ProtoMember(1)]
        public string Value { get; set; }
    }

    [ProtoContract]
    public class TestResult
    {
        [ProtoMember(1)]
        public CollectionWrapper First { get; set; }
    }

    [ProtoContract]
    public class CollectionWrapper
    {
        [ProtoMember(1)]
        public List<Wrapper> Collection { get; set; } = new List<Wrapper>();
    }
}

我正在使用以下版本的软件包:-

<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="Newtonsoft.Json" version="10.0.3" targetFramework="net47" />
  <package id="protobuf-net" version="2.3.4" targetFramework="net47" />
</packages>

这是我的结果:-

Foo.exe
Using json.net
Took: 12,000 ms
Allocated: 20,436 kb
Peak Working Set: 36,332 kb

Foo.exe 1
Using protobuf-net
Took: 5,203 ms
Allocated: 3,296,838 kb
Peak Working Set: 137,044 kb

如有任何帮助,我们将不胜感激。

非常感谢。

这是长度前缀强制缓冲的结果。这将在下一个 "major" 版本中完全重做(我有原型代码,它还没有准备好),以完全避免这个问题 - 使用一些巧妙的技巧来有效地计算所需的值提前。

在此期间,有一种方法可以防止这种缓冲:使用 "groups"。基本上,在 protobuf 中有两种编码子对象的方法——长度前缀(默认),或 start/end 哨兵。与 JASON 相比,您可以将这些哨兵视为 {},但在 protobuf 中。要切换到此,请将 DataFormat = DataFormat.Group 添加到所有子对象 [ProtoMember(...)] 属性,包括集合成员。这应该从根本上削减工作集,但是:它是一种不同的数据布局。如果 x-plat 是一个问题,大多数 protobuf 库将与组一起工作,但要明确:Google 已经决定 groups===bad(真可惜,我爱他们!),他们没有不再存在于 proto3 模式语法中 - 但它们在 proto2 中。

技术层面:

  • length-prefix 的编写成本更高(因为它需要预先计算),但是检查整个帧要解码的成本非常低
  • sentinels 的编写成本低得离谱,但要检查是否有整个帧要解码变得更加困难(因为您需要在每个字段的基础上进行健全性检查)

Google 显然更喜欢以更昂贵的写入为代价的廉价读取。这对 protobuf-net 的 v2 引擎的影响大于对 Google 库的影响,因为它们对大多数数据进行预编码的方式。 v3 引擎将是这个问题的 "cured",但我对此没有硬性的 ETA(我一直在为 v3 引擎试验即将推出的 corefx "pipelines" API,但那是很快就会发生;但是,我希望 v3 API 适合 与 "pipelines" 一起使用,因此现在的工作;很可能 v3 会"pipelines"之前发货很久了)。

现在,请尝试:

[ProtoContract]
public class Wrapper
{
    [ProtoMember(1)]
    public string Value { get; set; }
}

[ProtoContract]
public class TestResult
{
    [ProtoMember(1, DataFormat = DataFormat.Group)]
    public CollectionWrapper First { get; set; }
}

[ProtoContract]
public class CollectionWrapper
{
    [ProtoMember(1, DataFormat = DataFormat.Group)]
    public List<Wrapper> Collection { get; set; } = new List<Wrapper>();
}