Search code examples
c#arraysunity-game-enginecontiguousfixed-size-types

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


I have a task which in C would be trivial but which C# seems to make (intentionally?) impossible.

In C I would pre-allocate the entire data model of my simulation, via structs set up as a single, monolithic hierarchy, including fixed-size arrays of yet more structs, maybe containing more arrays. This is nigh-doable in C#, except for one thing...

In C#, we have the fixed keyword to specify fixed-size buffers (arrays) in each struct type - Cool. However, this supports only primitives as the fixed buffer element type, throwing a major spanner in these works of having a single monolithic, hierarchical and contiguously-allocated data model that begins to ensure optimal CPU cache access.

Other approaches I can see are the following:

  1. Use structs that allocate the array elsewhere through a separate new (which would seem to defeat contiguity entirely) - standard practice but not efficient.
  2. Use the fixed arrays of primitive types (say byte) but then have to marshal these back and forth when I want to change things... will this even work easily? Could be very tedious.
  3. Do (1) while assuming that the platform knows to moves things around for maximum contiguity.

I am using .NET 2.0 under Unity 5.6.


Solution

  • Without access to Memory<T>, ended up going with option (2), but no marshalling was necessary, only casting: use a fixed array of bytes in an unsafe struct and cast to/from these as follows:

    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 access is very fast, and with no marshalling or copies - is exactly what I wanted.

    If likely to be using 4-byte ints or floats for all your struct members, you might even do better to base your fixed buffer off such a type (uint is always a clean choice) - readily debuggable.


    UPDATE 2021

    I've revisited this topic this year, for prototyping in Unity 5 (due to fast compile / iteration times).

    It can be easier to stick with one very large byte array, and use this in managed code, rather than bothering with fixed + unsafe (by the way since C# 7.3 it is no longer necessary to use the fixed keyword every time to pin a fixed-size buffer in order to access it).

    With fixed we lose type-safety; this being a natural shortcoming of interop data - whether interop between native and managed; CPU and GPU; or between Unity main thread code and that used for the new Burst / Jobs systems. The same applies for managed byte buffers.

    Thus it can be easier to accept working with untyped managed buffers and writing offset + sizes yourself. fixed / unsafe offers (a little) more convenience, but not by much, since you equally have to specify compile-time struct field offsets and change these each time the data design changes. At least with managed VLAs, I can sum offsets in code, however this does mean these are not compile-time constants, thus losing some optimisations.

    The only real benefit of allocating a fixed buffer this way vs. a managed VLA (in Unity), is that with the latter, there is a chance the GC will move your entire data model somewhere else in mid-play, which could cause hiccups, though I've yet to see how serious this is in production.

    Managed arrays are not, however, directly supported by Burst.