Reputation: 8847
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:
new
(which would seem to defeat contiguity entirely) - standard practice but not efficient.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.I am using .NET 2.0 under Unity 5.6.
Upvotes: 2
Views: 1376
Reputation: 8847
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.
Upvotes: 1
Reputation: 5341
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?
Upvotes: 3