9

在我们的应用程序中,我们有一些数据结构,其中包含一个分块的字节列表(当前公开为List<byte[]>)。我们将字节分块,因为如果我们允许将字节数组放在大对象堆上,那么随着时间的推移,我们会遭受内存碎片的困扰。

我们还开始使用 Protobuf-net 序列化这些结构,使用我们自己生成的序列化 DLL。

然而,我们注意到 Protobuf-net 在序列化时创建了非常大的内存缓冲区。浏览源代码,它似乎可能在整个结构被写入之前无法刷新其内部缓冲区,List<byte[]>因为它需要在之后将总长度写入缓冲区的前面。

不幸的是,这首先取消了我们对字节进行分块的工作,并最终由于内存碎片给了我们 OutOfMemoryExceptions(异常发生在 Protobuf-net 试图将缓冲区扩展到超过 84k 的时候,这显然把它放在LOH,我们的整体进程内存使用率相当低)。

如果我对 Protobuf-net 工作原理的分析是正确的,有没有办法解决这个问题?


更新

根据马克的回答,这是我尝试过的:

[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.flushLockequals 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
{
}

然后writer.flushLock等于。2_DemandSpace()

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

4

2 回答 2

6

我将在这里阅读一些行之间的内容......因为List<T>(mapped as repeatedin protobuf parlance) 没有整体长度前缀,并且byte[](mapped as bytes) 有一个不应该导致额外缓冲的微不足道的长度前缀。所以我猜你实际上拥有的更像是:

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

这里需要缓冲一个length-prefix其实是在写的时候A.Foo,基本上就是声明“后面的复数数据就是”的值A.Foo)。幸运的是,有一个简单的解决方法:

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

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

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

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

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

于 2012-07-04T06:31:05.693 回答
3

补充您的编辑;[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 的一些返工)相关联,但应尽快提交。

于 2012-07-08T22:30:48.493 回答