在我们的应用中具有除其他事项包含的字节的分块列表(目前公开为一些数据结构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
等于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;}
}
此处,对于一个长度前缀来缓冲需求实际上是写入时A.Foo
,基本上申报 “下面复杂的数据是用于值A.Foo
”)。 幸运的是有一个简单的解决:
[ProtoMember(1, DataFormat=DataFormat.Group)]
public B Foo {get;set;}
这protobuf的2种充填技术之间的变化:
- 默认(谷歌的陈述偏好)是长度为前缀的,这意味着你会得到一个标志指示消息的长度跟随,则子消息负载
- 但也有使用启动标记时,子消息的有效载荷,和一个结束标记的选项
当使用第二种技术并不需要缓冲 ,因此:没有。 这并不意味着它会为相同的数据被写入略有不同的字节,但是protobuf网是非常宽容的,并愉快地序列化从两种格式在这里数据。 含义:如果您进行此更改,您仍然可以读取现有的数据,但新数据将使用开始/结束标记技术。
这就要求一个问题:为什么谷歌更喜欢长度前缀的方法吗? 也许这是因为它是通读场跳过时更有效(无论是通过原始阅读器API,或不想要的/意外的数据)使用长度前缀的方法时,你可以只读取长度前缀,然后就进步的流[n]的字节; 相比之下,与一个开始/结束标记,你仍然需要通过有效载荷抓取跳过数据,分别跳过子字段。 当然,如果您预计数据并希望将它读入你的对象,你几乎可以肯定是在做读取性能,这种理论上的区别并不适用。 此外,在谷歌protobuf的实施,因为它不是一个普通POCO模式工作时,有效载荷的大小是已知的,所以他们没有真正看到了同样的问题,写作时。
附加重新您的编辑; 的[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),但应尽快提交。