固定大小缓冲区的序列化



我有一个包含 14,400,360 个结构的数组,每个结构由 3 个字节的数据组成。

[ProtoContract]
struct A
{
[ProtoMember(1)]
private unsafe fixed byte[3] data;
}

不幸的是,protobuf-net 2.0.0.668 无法序列化固定大小的缓冲区,并在序列化时引发异常。 (类似于"FixedArray 没有序列化程序") 我认为答案在这里解释。

我目前的解决方法是将固定数组拆分为三个单独的字节并修复布局。

[ProtoContract]
[StructLayout(LayoutKind.Explicit, Size = 3, CharSet = CharSet.Ansi)]
struct A
{
[ProtoMember(1)]
[FieldOffset(0)]
private byte data;
[ProtoMember(2)]
[FieldOffset(1)]
private byte data1;
[ProtoMember(3)]
[FieldOffset(2)]
private byte data2;
}

问:我错过了一些黑魔法技巧或普通技巧,所以我不必手动拆分阵列?

在测量包含此数组的对象的堆使用情况时,它占用 43,201,160 字节或每个结构仅占用 3 个字节。 磁盘上的序列化文件占用 72,814,584 字节或每个结构大约 5.05 字节。

问:每个结构占用这些额外的 2 个字节是什么? 我还没有尝试过,但也许可以通过制作一个 3*14,400,360 字节的数组来减小序列化大小?(不得已而为之)

编辑:更正序列化文件的大小为 126,246,995 字节或每个结构 8.8 字节,而不是最初报告的每个结构 5.5 字节。

编辑:跟进使用此答案中的单成员技巧将文件大小减小到 90,952,228 字节或每个结构 6.3 字节。

有趣的问题。我喜欢有趣的问题!

在 protobuf 中没有一种完美的方法来表示这一点 - 因为预定义的格式没有固定大小数据类型的概念。我们有多种选择:

  • 每个元素使用一个字段,但这需要花费每个元素的标题+值,使大小加倍(对于大字段数字,或更多) - 不是一个很好的选择(作为参考,我认为您看到 5.05 的原因是因为它跳过了任何零值)
  • 使用以长度为前缀的块(bytesrepeated packed) - 但这仍然需要花费一个标头加上一个长度加上有效载荷 - 所以: 在您的情况下为 5 个字节; 还有一些尴尬的问题,如果我们要反序列化数据并且它超过3 个字节该怎么办,这不应该发生,但是: 我是一名图书馆作者,有很多不应该发生的事情: 发生
  • 使用单个固定大小的整数格式,因此:标头加 4 个字节 = 5 个字节
  • 使用单个变体整数格式,因此:标头加 1-4 个字节(24 位可以占用 1-4 个字节,因为 Varint 是 7 位加延续)

在所有这些中,所有这些周围还将有一个对象包装器,这将占用 2 个字节。

鉴于这些选项,我认为后者将是你最好的选择——你可以通过一个成员来完成:

[ProtoMember(1)] // varint by default
private uint SerializedValue {
get { /* pack bits from the field and return */ } }
set { /* unpack "value" into the field */ }
}

这大约是你今天能做的最好的事情;然而,对我来说,考虑改进vFuture是一个有趣的场景 - 也许可以让我们避免外部对象的开销;基本上将整个事情表示为单个二进制字符串 - 不需要内部字段标记。


但是,我想知道这里理想的解决方案是向上移动一个级别,以便数组/A列表本身可以被视为可抛弃和"打包"。这意味着我们基本上有这样的东西:

WriteFieldHeaderWithLengthPrefix(1, WireType.String, arr.Length * 3);
foreach(var item in arr) Append(item); // writes 3 bytes

和:

var arr = new A[ReadLengthPrefix() / 3];
for(int i = 0 ; i < arr.Length; i++)
arr[i] = Parse(item); // reads 3 bytes

这在当前代码中不可用,但在新代码中可能是可能的 - 我们有自定义序列化程序的概念。这当然是我探索的一个领域。

最新更新