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

时间:2018-01-20 16:01:11

标签: c# json.net protobuf-net

我最近的任务是探索在性能关键应用程序中使用protobuf-net。它目前使用Newtonsoft.Json,并且在大多数情况下,protobuf-net表现出了出色的性能提升。但在某些情况下,内存分配正在进行,我仍然不知道如何弄清楚发生了什么。

我已经整理了一个复制问题的小型控制台应用程序(问题最初是通过性能回归测试找到的)。由于显而易见的原因,我无法发布该确切的测试,但我有一个类似的例子;

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

任何帮助都将不胜感激。

非常感谢。

1 个答案:

答案 0 :(得分:1)

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

在此期间,有一种可用的方法来阻止这种缓冲:使用“群组”。基本上,有两种方法可以在protobuf中编码子对象 - 长度前缀(默认值)或开始/结束哨兵。与JASON相比,您可以将这些标记视为{},但在protobuf中。要切换到此项,请将DataFormat = DataFormat.Group添加到所有子对象[ProtoMember(...)]属性,包括集合成员。这应该从根本上削减工作集,但是:它是一个不同的数据布局。大多数protobuf库可以很好地与团队合作,如果x-plat是一个问题,但要明确:谷歌已经决定团体===糟糕(这是一个耻辱,我爱他们!),他们不再存在proto3架构语法 - 但它们在proto2中。

在技术层面:

  • 长度前缀的写入成本更高(因为它需要预先计算),但是检查你有一整帧要解码是非常便宜的
  • 哨兵编写起来非常便宜,但要检查你是否需要整个帧进行解码(因为你需要在每个字段的基础上进行健全检查),这样会更加困难。
谷歌显然更喜欢便宜的读取而牺牲更昂贵的写入。这会影响protobuf-net的v2引擎而不是影响Google的库,因为它们对大多数数据进行了预编码。 v3引擎将“解决”这个问题,但我没有硬ETA(我一直在试验v3引擎即将推出的corefx“管道”API,但这不会很快发生;但是,我希望v3 API 适合与“管道”一起使用,因此现在工作;很可能v3将在“管道”之前发布很长时间。)

目前,请尝试:

[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>();
}