关于C#结构体的内存/序列化开销

7

我的代码像这样:

[Serializable]
[StructLayout(LayoutKind.Sequential,Pack=1)]
struct Foo
{
    public byte Bar;            
    public Foo(byte b){Bar=b;}
}
public static void Main (string[] args)
{
    Foo[] arr = new Foo[1000];
    for (int i = 0; i < 1000; i++) {
        arr[i]=new Foo(42);            
    }
    var fmt = new BinaryFormatter();
    using(FileStream f= File.Create("test.bin")){
        fmt.Serialize(f,arr);
    }
    Console.WriteLine (new FileInfo("test.bin").Length);
}

结果bin文件大小为10095字节。为什么我的Foo结构体会占用这么多字节?每个结构体的9个字节开销是什么意思?
PS:我正在编写一个汉字查找库(大约涉及70000个字符的信息),db4o或其他可嵌入的数据库(如sqlite)有点臃肿。我想将所有信息存储在纯字符串格式中,这是最友好的内存方式,但灵活性较差。我想将信息保存在列表中,并将它们作为二进制序列化存储到档案中,我选择了DotNetZip进行归档。但序列化开销是一个意外的障碍。更好的序列化解决方案将是不错的,否则我必须以纯字符串格式保存信息并通过硬编码进行解析。

1
其中一部分可能是序列化开销。 - The Scrum Meister
2
如果10KB是相关的,那么你可能正在错误地解决这个问题。 - Cody Gray
如果您正在寻求优化序列化速度和大小,请查看Google Protocol Buffers。 - Julien
1
这不是关于10kb的问题,那只是一个例子,在我真正的应用程序中,大约有100万或甚至1000万个对象。我发现每个结构体对象会有9到12字节的开销,这实在太大了。 - Need4Steed
2个回答

14
并不是Foo结构体本身很“大”,而是你观察到的是二进制序列化格式本身的开销。该格式包含一个标头,用于描述对象图形的信息,用于描述数组的信息,用于描述类型和程序集信息的字符串等信息。也就是说,它包含了足够的信息,以便BinaryFormatter.Deserialize能够像您所期望的那样还原出一个Foo数组。
有关更多信息,请参阅详细说明该格式的规范:http://msdn.microsoft.com/en-us/library/cc236844(PROT.10).aspx 根据您更新的问题进行编辑:
如果您想要简单地将结构的内容写入流中,则可以在不安全的上下文中轻松完成此操作(此代码基于您的示例)。
使用小型数组写出每个Foo:
unsafe 
{
    byte[] data = new byte[sizeof(Foo)];

    fixed (Foo* ptr = arr)
    {
        for (int i = 0; i < arr.Length; ++i)
        {
            Marshal.Copy((IntPtr)ptr + i, data, 0, data.Length);
            f.Write(data, 0, data.Length);
        }
    }
}

或者使用一个足够大的数组来写出所有的Foos:

unsafe 
{
    byte[] data = new byte[sizeof(Foo) * arr.Length];

    fixed (Foo* ptr = arr)
    {
        Marshal.Copy((IntPtr)ptr, data, 0, data.Length);
        f.Write(data, 0, data.Length);
    }
}

根据你的示例,这将输出1000个值为42的字节。

然而,这种方法有一些缺点。如果您熟悉像C这样的语言中结构的编写,其中一些应该很明显:

  • 如果您在与用于写入数据的不同字节顺序的计算机上读取数据,则无法获得预期的结果。您需要定义一个预期的字节顺序并自己处理转换。
  • Foo 不能包含引用类型的字段。这意味着您需要使用长度字段+ char 的固定大小缓冲区,而不是 System.String; 这可能非常麻烦。
  • 如果 Foo 包含指针类型或 IntPtr / UIntPtr,则结构的大小可能因机器架构而异。 如果可能,要避免使用这些类型。
  • 您需要应用自己的版本控制方案,以便可以有一定的信心读回的数据与预期的结构定义匹配。任何对结构布局的更改都需要新版本。

BinaryFormatter 可以解决这些问题,但会带来你观察到的空间开销。 它旨在以安全的方式在计算机之间交换数据。 如果您不想使用 BinaryFormatter,则需要定义自己的文件格式并处理该格式的读写,或者使用最适合您需求的第三方序列化库(我将把这些库的研究留给您自己完成)。


谢谢。我考虑过字节数组,但在我的项目中会涉及到不同长度的字符串和列表/数组,结构体不会是独占值类型容器。因此,我宁愿以解析格式化的方式实现它,而不是内存块复制,以最小化内存和存储消耗。 - Need4Steed

1
如果你想测量消耗了多少内存,可以使用以下代码:
long nTotalMem1 = System.GC.GetTotalMemory(true);
Foo[] arr = new Foo[1000];
for (int i = 0; i < 1000; i++)
{
    arr[i] = new Foo(42);
}
long nTotalMem2 = System.GC.GetTotalMemory(true);
Console.WriteLine("Memory consumption: " + (nTotalMem2 - nTotalMem1) + " bytes");

提示:1012字节。:)

编辑:也许更可靠的方法是使用Marshal.SizeOf方法:

Console.WriteLine("Size of one instance: " + Marshal.SizeOf(arr[0]) + " bytes");

这对我返回了1个字节的结果,当向结构体添加另一个字段时,它返回了2个字节,因此看起来非常可靠。


为什么我的结果是4096?这太不可思议了。 - Need4Steed
@需要确实奇怪....发布您拥有该代码的方法的完整代码,我会尝试查看。(您可以编辑原始问题) - Shadow The Spring Wizard
@需要抱歉,也许这只适用于Windows,我真的不知道。我会编辑并提供另一个选项。现在请查看我的编辑。 :) - Shadow The Spring Wizard
感谢你的帮助。我想结构体肯定不应该占用太多内存。但序列化开销仍未解决,我已经尝试了 protobuf.net,但当涉及字符串或其他任意类型时,它甚至比内置的还要糟糕。 - Need4Steed
@需要什么是您的最终目标?您会用这个结构做什么?为什么需要序列化它? - Shadow The Spring Wizard
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接