简体   繁体   中英

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 CI 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.

Please take a look on Span<T> and Memory<T> features of C# 7.2. I think that would solve your problem.

What is the difference between Span<T> and Memory<T> in C# 7.2?

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 int s or float s 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 .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM