-->

存储器使用串行化分块字节数组与protobuf网(Memory usage serializing

2019-06-26 00:51发布

在我们的应用中具有除其他事项包含的字节的分块列表(目前公开为一些数据结构List<byte[]> 因为如果我们允许字节数组要对大对象堆放那么随着时间的推移,我们从内存碎片遭受我们字节块了。

我们使用protobuf网连载这些结构,用我们自己生成的序列化DLL也开始了。

不过,我们注意到,protobuf网正在创造非常大的内存缓冲区,而序列化。 通过源代码掠看起来也许不能刷新其内部缓冲器,直至整个List<byte[]>结构已被写入,因为它需要在缓冲器的前后来写入的总长度。

这不幸的是撤销我们的工作摆在首位组块的字节数,并最终使我们因OutOfMemoryExceptions内存碎片(发生在这里protobuf网正试图超过84K的缓存扩大到,这显然把它放在时间异常LOH,我们的整体进程内存使用是相当低的)。

如果我的protobuf网是如何工作的分析是正确的,是有解决这个问题的方法吗?


更新

根据马克的答案,这里是我已经试过:

[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等于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等于2DemandSpace()

我猜有一个明显的一步,我已经在这里错过了与派生类型呢?

Answer 1:

我要去这里一些字里行间...因为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;}
}

此处,对于一个长度前缀来缓冲需求实际上是写入时A.Foo ,基本上申报 “下面复杂的数据是用于值A.Foo ”)。 幸运的是有一个简单的解决:

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

这protobuf的2种充填技术之间的变化:

  • 默认(谷歌的陈述偏好)是长度为前缀的,这意味着你会得到一个标志指示消息的长度跟随,则子消息负载
  • 但也有使用启动标记时,子消息的有效载荷,和一个结束标记的选项

当使用第二种技术并不需要缓冲 ,因此:没有。 这并不意味着它会为相同的数据被写入略有不同的字节,但是protobuf网是非常宽容的,并愉快地序列化从两种格式在这里数据。 含义:如果您进行此更改,您仍然可以读取现有的数据,但新数据将使用开始/结束标记技术。

这就要求一个问题:为什么谷歌更喜欢长度前缀的方法吗? 也许这是因为它是通读场跳过更有效(无论是通过原始阅读器API,或不想要的/意外的数据)使用长度前缀的方法时,你可以只读取长度前缀,然后就进步的流[n]的字节; 相比之下,与一个开始/结束标记,你仍然需要通过有效载荷抓取跳过数据,分别跳过子字段。 当然,如果您预计数据并希望将它读入你的对象,你几乎可以肯定是在做读取性能,这种理论上的区别并不适用。 此外,在谷歌protobuf的实施,因为它不是一个普通POCO模式工作时,有效载荷的大小是已知的,所以他们没有真正看到了同样的问题,写作时。



Answer 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),但应尽快提交。



文章来源: Memory usage serializing chunked byte arrays with Protobuf-net