Jam*_*ley 8 c# chunking protobuf-net large-object-heap
在我们的应用程序中,我们有一些数据结构,其中包含一个分块的字节列表(当前公开为a List<byte[]>).我们将字节大块化,因为如果我们允许将字节数组放在大对象堆上,那么随着时间的推移,我们会遇到内存碎片.
我们也开始使用Protobuf-net来序列化这些结构,使用我们自己生成的序列化DLL.
但是我们注意到Protobuf-net在序列化时会创建非常大的内存缓冲区.浏览源代码看起来似乎它可能无法刷新其内部缓冲区,直到整个List<byte[]>结构被写入,因为它需要在缓冲区前面写入总长度.
不幸的是,这首先解决了我们的工作,首先将字节分块,最终由于内存碎片而给我们OutOfMemoryExceptions(异常发生在Protobuf-net尝试将缓冲区扩展到84k以上时,这显然是在LOH,我们的整体进程内存使用率相当低.
如果我对Protobuf-net如何工作的分析是正确的,那么有没有解决这个问题的方法呢?
更新
根据Marc的回答,这是我尝试过的:
[ProtoContract]
[ProtoInclude(1, typeof(A), DataFormat = DataFormat.Group)]
public class ABase
{
}
[ProtoContract]
public class A : ABase
{
[ProtoMember(1, DataFormat = DataFormat.Group)]
public B B
{
get;
set;
}
}
[ProtoContract]
public class B
{
[ProtoMember(1, DataFormat = DataFormat.Group)]
public List<byte[]> Data
{
get;
set;
}
}
Run Code Online (Sandbox Code Playgroud)
然后序列化它:
var a = new A();
var b = new B();
a.B = b;
b.Data = new List<byte[]>
{
Enumerable.Range(0, 1999).Select(v => (byte)v).ToArray(),
Enumerable.Range(2000, 3999).Select(v => (byte)v).ToArray(),
};
var stream = new MemoryStream();
Serializer.Serialize(stream, a);
Run Code Online (Sandbox Code Playgroud)
但是,如果我ProtoWriter.WriteBytes()在它调用DemandSpace()方法底部的步骤中插入断点并进入DemandSpace(),我可以看到缓冲区没有被刷新,因为writer.flushLock等于1.
如果我像这样为ABase创建另一个基类:
[ProtoContract]
[ProtoInclude(1, typeof(ABase), DataFormat = DataFormat.Group)]
public class ABaseBase
{
}
[ProtoContract]
[ProtoInclude(1, typeof(A), DataFormat = DataFormat.Group)]
public class ABase : ABaseBase
{
}
Run Code Online (Sandbox Code Playgroud)
然后,writer.flushLock等于2在DemandSpace().
我猜这里有一个明显的步骤,我错过了派生类型吗?
我将在这里阅读一些行...因为List<T>(repeated在protobuf用语中映射)没有总长度前缀,并且byte[](映射为bytes)有一个简单的长度前缀,不应该导致额外的缓冲.所以我猜你实际拥有的更像是:
[ProtoContract]
public class A {
[ProtoMember(1)]
public B Foo {get;set;}
}
[ProtoContract]
public class B {
[ProtoMember(1)]
public List<byte[]> Bar {get;set;}
}
Run Code Online (Sandbox Code Playgroud)
这里,缓冲长度前缀的需要实际上是在写入时A.Foo,基本上是为了声明 "以下复杂数据是值A.Foo").幸运的是有一个简单的解决方法:
[ProtoMember(1, DataFormat=DataFormat.Group)]
public B Foo {get;set;}
Run Code Online (Sandbox Code Playgroud)
这在protobuf中的两种包装技术之间发生了变化:
当使用第二种技术时,它不需要缓冲,因此:它不需要缓冲.这意味着它将为相同的数据写出稍微不同的字节,但是protobuf-net非常宽容,并且很乐意在这里对这两种格式的数据进行反序列化.含义:如果进行此更改,您仍然可以读取现有数据,但新数据将使用开始/结束标记技术.
这就提出了一个问题:谷歌为什么更喜欢长度前缀方法?可能这是因为在使用长度前缀方法读取跳过字段(通过原始读取器API或作为不需要的/意外数据)时更有效,因为您可以只读取长度前缀,然后只是进行流[n]字节; 相反,要使用开始/结束标记跳过数据,您仍需要爬过有效负载,单独跳过子字段.当然,如果您期望数据并希望将其读入您的对象,那么读取性能的这种理论差异就不适用了,您几乎肯定会这样做.此外,在谷歌protobuf实现中,因为它不使用常规POCO模型,有效载荷的大小已经知道,所以他们在写作时并没有真正看到相同的问题.