在 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>();
}
我最近的任务是探索 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>();
}