简体   繁体   English

使用Protobuf-net序列化分块字节数组的内存使用情况

[英]Memory usage serializing chunked byte arrays with Protobuf-net

In our application we have some data structures which amongst other things contain a chunked list of bytes (currently exposed as a List<byte[]> ). 在我们的应用程序中,我们有一些数据结构,其中包含一个分块的字节列表(当前公开为List<byte[]> )。 We chunk bytes up because if we allow the byte arrays to be put on the large object heap then over time we suffer from memory fragmentation. 我们将字节大块化,因为如果我们允许将字节数组放在大对象堆上,那么随着时间的推移,我们会遇到内存碎片。

We've also started using Protobuf-net to serialize these structures, using our own generated serialization DLL. 我们也开始使用Protobuf-net来序列化这些结构,使用我们自己生成的序列化DLL。

However we've noticed that Protobuf-net is creating very large in-memory buffers while serializing. 但是我们注意到Protobuf-net在序列化时会创建非常大的内存缓冲区。 Glancing through the source code it appears that perhaps it can't flush its internal buffer until the entire List<byte[]> structure has been written because it needs to write the total length at the front of the buffer afterwards. 浏览源代码时,似乎它可能无法刷新其内部缓冲区,直到整个List<byte[]>结构被写入,因为它需要在缓冲区前面写入总长度。

This unfortunately undoes our work with chunking the bytes in the first place, and eventually gives us OutOfMemoryExceptions due to memory fragmentation (the exception occurs at the time where Protobuf-net is trying to expand the buffer to over 84k, which obviously puts it on the LOH, and our overall process memory usage is fairly low). 不幸的是,这首先解决了我们的工作,首先将字节分块,最终由于内存碎片而给我们OutOfMemoryExceptions(异常发生在Protobuf-net尝试将缓冲区扩展到84k以上时,这显然是在LOH,我们的整体进程内存使用率相当低。

If my analysis of how Protobuf-net is working is correct, is there a way around this issue? 如果我对Protobuf-net如何工作的分析是正确的,那么有没有解决这个问题的方法呢?


Update 更新

Based on Marc's answer, here is what I've tried: 根据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;
    }
}

Then to serialize it: 然后序列化它:

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);

However if I stick a breakpoint in ProtoWriter.WriteBytes() where it calls DemandSpace() towards the bottom of the method and step into DemandSpace() , I can see that the buffer isn't being flushed because writer.flushLock equals 1 . 但是,如果我在ProtoWriter.WriteBytes()中使用断点来调用DemandSpace()到方法的底部并进入DemandSpace() ,我可以看到缓冲区没有被刷新,因为writer.flushLock等于1

If I create another base class for ABase like this: 如果我像这样为ABase创建另一个基类:

[ProtoContract]
[ProtoInclude(1, typeof(ABase), DataFormat = DataFormat.Group)]
public class ABaseBase
{
}

[ProtoContract]
[ProtoInclude(1, typeof(A), DataFormat = DataFormat.Group)]
public class ABase : ABaseBase
{
}

Then writer.flushLock equals 2 in DemandSpace() . 然后,在DemandSpace() writer.flushLock等于2

I'm guessing there is an obvious step I've missed here to do with derived types? 我猜这里有一个明显的步骤,我错过了派生类型吗?

I'm going to read between some lines here... because List<T> (mapped as repeated in protobuf parlance) doesn't have an overall length-prefix, and byte[] (mapped as bytes ) has a trivial length-prefix that shouldn't cause additional buffering. 我将在这里阅读一些行...因为List<T> (在protobuf用语中映射为repeated )没有整体长度前缀,而byte[] (映射为bytes )具有一个微不足道的长度 -不应导致额外缓冲的前缀。 So I'm guessing what you actually have is more like: 所以我猜你实际拥有的更像是:

[ProtoContract]
public class A {
    [ProtoMember(1)]
    public B Foo {get;set;}
}
[ProtoContract]
public class B {
    [ProtoMember(1)]
    public List<byte[]> Bar {get;set;}
}

Here, the need to buffer for a length-prefix is actually when writing A.Foo , basically to declare "the following complex data is the value for A.Foo "). 这里,缓冲长度前缀的需要实际上是在编写A.Foo ,基本上是为了声明 “以下复杂数据是A.Foo的值”)。 Fortunately there is a simple fix: 幸运的是有一个简单的解决方法:

[ProtoMember(1, DataFormat=DataFormat.Group)]
public B Foo {get;set;}

This changes between 2 packing techniques in protobuf: 这在protobuf中的两种包装技术之间发生了变化:

  • the default (google's stated preference) is length-prefixed, meaning you get a marker indicating the length of the message to follow, then the sub-message payload 默认(谷歌声明的偏好)是长度前缀,这意味着你得到一个标记,指示要遵循的消息的长度,然后是子消息有效负载
  • but there is also an option to use a start-marker, the sub-message payload, and an end-marker 但是也可以选择使用开始标记,子消息有效负载和结束标记

When using the second technique it doesn't need to buffer , so: it doesn't. 当使用第二种技术时, 它不需要缓冲 ,因此:它不需要缓冲 This does mean it will be writing slightly different bytes for the same data, but protobuf-net is very forgiving, and will happily deserialize data from either format here. 这意味着它将为相同的数据写出稍微不同的字节,但是protobuf-net非常宽容,并且很乐意在这里对这两种格式的数据进行反序列化。 Meaning: if you make this change, you can still read your existing data, but new data will use the start/end-marker technique. 含义:如果进行此更改,您仍然可以读取现有数据,但新数据将使用开始/结束标记技术。

This demands the question: why do google prefer the length-prefix approach? 这就提出了一个问题:谷歌为什么更喜欢长度前缀方法? Probably this is because it is more efficient when reading to skip through fields (either via a raw reader API, or as unwanted/unexpected data) when using the length-prefix approach, as you can just read the length-prefix, and then just progress the stream [n] bytes; 可能这是因为在使用长度前缀方法读取跳过字段(通过原始读取器API或作为不需要的/意外数据)时更有效,因为您可以只读取长度前缀,然后只是进行流[n]字节; by contrast, to skip data with a start/end-marker you still need to crawl through the payload, skipping the sub-fields individually. 相反,要使用开始/结束标记跳过数据,您仍需要爬过有效负载,单独跳过子字段。 Of course, this theoretical difference in read performance doesn't apply if you expect that data and want to read it into your object, which you almost certainly do. 当然,如果您期望数据并希望将其读入您的对象,那么读取性能的这种理论差异就不适用了,您几乎肯定会这样做。 Also, in the google protobuf implementation, because it isn't working with a regular POCO model, the size of the payloads are already known, so they don't really see the same issue when writing. 此外,在谷歌protobuf实现中,因为它不使用常规POCO模型,有效载荷的大小已经知道,所以他们在写作时并没有真正看到相同的问题。

Additional re your edit; 额外的你的编辑; the [ProtoInclude(..., DataFormat=...)] looks like it simply wasn't being processed. [ProtoInclude(..., DataFormat=...)]看起来好像没有被处理。 I have added a test for this in my current local build, and it now passes: 我已经在我当前的本地版本中添加了一个测试,它现在通过了:

[Test]
public void Execute()
{

    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();
    var model = TypeModel.Create();
    model.AutoCompile = false;
#if DEBUG // this is only available in debug builds; if set, an exception is
  // thrown if the stream tries to buffer
    model.ForwardsOnly = true;
#endif
    CheckClone(model, a);
    model.CompileInPlace();
    CheckClone(model, a);
    CheckClone(model.Compile(), a);
}
void CheckClone(TypeModel model, A original)
{
    int sum = original.B.Data.Sum(x => x.Sum(b => (int)b));
    var clone = (A)model.DeepClone(original);
    Assert.IsInstanceOfType(typeof(A), clone);
    Assert.IsInstanceOfType(typeof(B), clone.B);
    Assert.AreEqual(sum, clone.B.Data.Sum(x => x.Sum(b => (int)b)));
}

This commit is tied into some other, unrelated refactorings (some rework for WinRT / IKVM), but should be committed ASAP. 此提交与其他一些不相关的重构(WinRT / IKVM的一些返工)相关联,但应该尽快提交。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM