【问题标题】:A good way to serialize a known array of small structs序列化已知小结构数组的好方法
【发布时间】:2021-06-08 08:38:01
【问题描述】:

我有大量的小结构,我想序列化到文件中。

结构:

public struct Voxel
{
  public byte density;
  public byte material;
}

虽然有很多序列化库可以非常有效地进行一般序列化,但我怀疑我们可以在磁盘大小序列化/反序列化速度方面做得更好>,假设我们知道并控制这个结构。

这个结构是非常最终的,所以我们可以不用许多序列化库支持的花哨的版本控制。

从我的搜索来看,Marshal 似乎是一个不错的方法来做这样的事情,但我不想担心字节序之类的事情。

所以我想知道,有什么好的方法可以序列化这些数据。假设数组大小可以在 100 到 1 百万之间?

(还假设我们不害怕以不同的格式存储它们,这样 RLE 可以进一步减少磁盘大小。)

【问题讨论】:

  • "endianness" 在这里实际上并不适用 - 只有当您有多字节值时才会成为问题;对于两个单独的 byte 值:你很好

标签: c# serialization deserialization


【解决方案1】:

假设您使用的是最近的框架:跨度是您的朋友。作为编写它们的简单方式:

Voxel[] arr = ...
var bytes = MemoryMarshal.Cast<Voxel, byte>(arr);
using (var s = File.OpenWrite("some.path"))
{
    s.Write(bytes);
}

阅读有点困难,但并不难:

Voxel[] arr;
using (var s = File.OpenRead("some.path"))
{
    int len = checked((int)(s.Length / Unsafe.SizeOf<Voxel>())), read;
    arr = new Voxel[len];
    var bytes = MemoryMarshal.Cast<Voxel, byte>(arr);
    while (!bytes.IsEmpty && (read = s.Read(bytes)) > 0)
    {
        bytes = bytes.Slice(read);
    }
}

请注意,这假定您希望以向量 (Voxel[]) 开头;如果您乐于在兔子洞中更进一步,“内存映射文件”也是一个选项,再次使用Span&lt;T&gt;(或Memory&lt;T&gt;) - 然后它就变成了真正的零副本(您的实时数据文件,通过操作系统魔法)。

【讨论】:

  • Thx,不幸的是我还在使用 .net 标准 2.0,所以问题是我们是否可以将这些 API 与 backport dll 一起使用,并且不会与现有的包依赖项冲突。如果在 2.0 领域有一些替代方案,我很乐意听到。
  • 这感觉很傻,但我找不到合适的 DLL 来让 FileStream.Write(ReadOnlySpan&lt;byte&gt;) API 在 .Net Standard 2.0 上工作
  • @bitinn 它位于 .net core 3+ 和 netstandard 2.1 IIRC 中;虽然如果是我,我会以 .net 5 或更高版本为目标
  • 这是我透露我的平台是 Unity 引擎的地方,所以事情有点复杂。
  • @bitinn 这就是我说在问题中包含它会很好的时候:) 一个固定/不安全的指针强制应该可以工作,但是你需要将数据复制到一个字节[ ] 使其工作:/
【解决方案2】:

对于未来的读者,我想出了一个解决方案,让我可以在 Unity Engine 中使用 @Marc Gravell 提出的解决方案,它只允许 .Net Standard 2.0;

诀窍是从 Microsoft 获得高性能软件包:

https://docs.microsoft.com/en-us/windows/communitytoolkit/high-performance/introduction

这意味着您可以从 UWP 或旧版 .NET Framework 应用程序、使用 Unity 编写的游戏、使用 Xamarin 的跨平台移动应用程序到 .NET Standard 库和现代 .NET Core 2.1 或 .NET Core 3.1 的任何应用程序中使用它应用程序。

通过扩展方法支持Stream.WriteStream.ReadSpan&lt;T&gt;

https://docs.microsoft.com/en-us/dotnet/api/microsoft.toolkit.highperformance.extensions.streamextensions?view=win-comm-toolkit-dotnet-6.1

我还比较了二进制序列化的磁盘大小:

  • 内存中:8KB(16x16x16 数组)
  • 消息包格式:97KB
  • MemoryMarshal.Cast:8KB

所以它按预期工作!

【讨论】:

  • 我有looked it up,并且:这个库通过租用一个数组并执行从输入到数组的内存复制来工作;有两点:1)它会比必要的慢,因为额外的内存副本(虽然我认为在你的情况下这是可以避免的),2)它根据你的输入大小使用缓冲区,但请注意数组池是封顶 - 超过一定的大小它只是分配每次,所以对于大的输入,这可能会导致分配问题
  • 我并不是说“不要使用它”——但是,我建议添加一些切片/循环代码,这样您就不会调用 ReadWrite 整个slice - 相反,每次调用只给它 64KiB 的数据(并在循环中调用)
  • @MarcGravell 谢谢提示,我想知道您对这种方法有何看法?似乎我无法避免在 2.0 土地上进行某种分配? stackoverflow.com/questions/3278827/…
  • 另外我现在正在阅读更多关于 ArrayPool 限制的信息,对于我的用例 (1024x1024) 来说似乎有点低:adamsitnik.com/Array-Pool
  • “关于这种方法吗?” - 很高兴发表意见,但是:你问的是什么方面?无需使用 Marshal 步骤 - span 强制转换免费;您在某些时候需要byte[] 用于Read/Write API - 并且数组池可以满足此要求只要您保持大小合理 - 但是:它不需要一次适合您的数据!您可以使用适度的缓冲区并非常轻松地循环
猜你喜欢
  • 1970-01-01
  • 2015-06-23
  • 1970-01-01
  • 1970-01-01
  • 2022-10-23
  • 1970-01-01
  • 2015-11-20
  • 2017-10-15
  • 1970-01-01
相关资源
最近更新 更多