C# 中具有固定大小数组的连续分层结构内存?

Contiguous hierarchical struct memory with fixed-size arrays in C#?

我有一个任务,在 C 中是微不足道的,但在 C# 中似乎(故意?)不可能完成。

在 C 中,我会通过将结构设置为单个整体层次结构来预先分配模拟的整个数据模型,包括更多结构的固定大小数组,可能包含更多数组。这在 C# 中几乎是可行的,除了一件事...

在 C# 中,我们有 fixed 关键字来在每个结构类型中指定固定大小的缓冲区(数组)——很酷。然而,这支持 only 基元作为固定缓冲区元素类型,在这些工作中抛出了一个主要的障碍,即拥有一个单一的、分层的和连续分配的数据模型,开始确保最佳 CPU缓存访问。

我能看到的其他方法如下:

  1. 使用通过单独的 new 将数组分配到别处的结构(这似乎完全破坏了连续性)- 标准做法但效率不高。
  2. 使用原始类型的固定数组(比如 byte),但是当我想改变一些东西时必须来回编组这些……这会很容易工作吗?可能会很乏味。
  3. 执行 (1),同时假设平台知道移动事物以获得最大连续性。

我在 Unity 5.6 下使用 .NET 2.0。

请查看 C# 7.2 的 Span<T>Memory<T> 功能。我想这会解决你的问题。

无法访问 Memory<T>,最终选择了选项 (2),但不需要编组,仅进行转换:在 unsafe struct 中使用 fixed 字节数组,并且铸造 to/from 这些如下:

using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using UnityEngine;
    
public class TestStructWithFixed : MonoBehaviour
{
    public const int MAX = 5;
    public const int SIZEOF_ELEMENT = 8;
    
    public struct Element
    {
        public uint x;
        public uint y;
        //8 bytes
    }
    
    [StructLayout(LayoutKind.Sequential, Pack = 1)]
    public unsafe struct Container
    {
        public int id; //4 bytes
        public unsafe fixed byte bytes[MAX * SIZEOF_ELEMENT];
    }
    
    public Container container;
    
    void Start ()
    {
        Debug.Log("SizeOf container="+Marshal.SizeOf(container));
        Debug.Log("SizeOf element  ="+Marshal.SizeOf(new Element()));
        
        unsafe
        {
            Element* elements;
            fixed (byte* bytes = container.bytes)
            {
                elements = (Element*) bytes;
                
                //show zeroed bytes first...
                for (int i = 0; i < MAX; i++)
                    Debug.Log("i="+i+":"+elements[i].x);
                
                //low order bytes of Element.x are at 0, 8, 16, 24, 32 respectively for the 5 Elements
                bytes[0 * SIZEOF_ELEMENT] = 4;
                bytes[4 * SIZEOF_ELEMENT] = 7;
            }
            elements[2].x = 99;
            //show modified bytes as part of Element...
            for (int i = 0; i < MAX; i++)
                Debug.Log("i="+i+":"+elements[i].x); //shows 4, 99, 7 at [0], [2], [4] respectively
        }
    }
}

unsafe 访问速度非常快,而且没有编组或复制 - 这正是我想要的。

如果您的所有 struct 成员可能使用 4 字节 ints 或 floats,您甚至可以更好地使用 fixed 缓冲区关闭这样的类型(uint 始终是一个干净的选择)- 易于调试。


2021 年更新

为了在 Unity 5 中制作原型(由于快速编译/迭代时间),我今年重新讨论了这个主题。

坚持使用一个非常大的字节数组并在托管代码中使用它会更容易,而不是费心使用 fixed + unsafe(顺便说一下,自 C# 7.3 it is no longer necessary to use the fixed keyword every time to pin a fixed-size buffer 以便访问它)。

fixed 我们失去了类型安全;这是互操作数据的自然缺点——无论是本机数据还是托管数据之间的互操作; CPU 和 GPU;或者在 Unity 主线程代码和用于新的 Burst / Jobs 系统的代码之间。这同样适用于托管字节缓冲区。

因此,可以更容易地接受使用非类型化托管缓冲区并自己编写偏移量 + 大小。 fixed / unsafe 提供了(一点)更多便利,但不是很多,因为您同样必须指定编译时结构字段偏移量并在每次数据设计更改时更改这些。至少对于托管 VLA,我可以在代码中求和偏移量,但这确实意味着它们不是编译时常量,因此失去了一些优化。

与托管 VLA(在 Unity 中)相比,以这种方式分配 fixed 缓冲区的唯一真正好处是,对于后者,GC 有可能将您的整个数据模型移动到其他地方在播放过程中,这可能会导致打嗝,但我还没有看到这在生产中有多严重。

托管数组 are not, however, directly supported by Burst