【问题标题】:Memory usage serializing chunked byte arrays with Protobuf-net使用 Protobuf-net 序列化分块字节数组的内存使用
【发布时间】:2012-07-04 05:55:37
【问题描述】:

在我们的应用程序中,我们有一些数据结构,其中包含一个分块的字节列表(当前公开为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;
    }
}

然后序列化它:

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

但是,如果我在 ProtoWriter.WriteBytes() 中插入一个断点,它在方法底部调用 DemandSpace() 并进入 DemandSpace(),我可以看到缓冲区没有被刷新,因为 writer.flushLock 等于 @987654329 @。

如果我像这样为 ABase 创建另一个基类:

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

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

那么writer.flushLock 等于2 中的DemandSpace()

我猜我在这里错过了一个与派生类型有关的明显步骤?

【问题讨论】:

    标签: c# protobuf-net chunking large-object-heap


    【解决方案1】:

    我将在这里阅读几行之间...因为List&lt;T&gt;(在protobuf用语中映射为repeated)没有整体长度前缀,而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;}
    }
    

    这里,需要缓冲一个长度前缀实际上是在写A.Foo时,基本上是为了声明“下面的复杂数据是A.Foo的值”)。幸运的是,有一个简单的解决方法:

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

    这在 protobuf 中的两种打包技术之间发生了变化:

    • 默认(谷歌声明的偏好)是长度前缀,这意味着你会得到一个标记,指示要遵循的消息的长度,然后是子消息有效负载
    • 但也可以选择使用开始标记、子消息负载和结束标记

    当使用第二种技术时它不需要缓冲,所以:它不需要。这确实意味着它将为相同的数据写入稍微不同的字节,但 protobuf-net 非常宽容,并且会很乐意在这里反序列化 either 格式的数据。含义:如果您进行此更改,您仍然可以读取现有数据,但新数据将使用开始/结束标记技术。

    这就提出了一个问题:为什么 google 更喜欢长度前缀方法? 可能这是因为在使用长度前缀方法时在阅读时跳过字段(通过原始阅读器 API,或作为不需要/意​​外的数据)更有效,因为您可以只读取长度前缀,然后只处理流 [n] 个字节;相比之下,要使用开始/结束标记跳过数据,您仍然需要爬取有效负载,分别跳过子字段。当然,如果您预期该数据并希望将其读入您的对象,则这种读取性能的理论差异并不适用,您几乎肯定会这样做。此外,在 google protobuf 实现中,由于它不适用于常规 POCO 模型,因此负载的大小是已知的,因此在编写时它们并没有真正看到相同的问题。

    【讨论】:

    • 感谢您的快速回复。您对我们的数据结构的猜测是正确的。我是否正确地说我们需要将包含对 A 的引用的任何属性的 DataFormat 更改为 Group ,等等直到对象图的根?而且这种更改也需要在相关的 ProtoInclude 属性上进行?
    • @James 基本上是的。嗯...也许我应该为此添加一个模型级默认值!
    • 我已经更新了我的问题,尝试使用 DataFormat.Group 来解决问题,但我仍然无法刷新缓冲区。对不起,如果我是个白痴..
    【解决方案2】:

    补充您的编辑; [ProtoInclude(..., DataFormat=...)] 看起来根本没有被处理。我在当前的本地构建中为此添加了一个测试,现在它通过了:

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

    此提交与其他一些不相关的重构(WinRT / IKVM 的一些返工)相关联,但应尽快提交。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-06-25
      相关资源
      最近更新 更多