Memory alignment and Component design in Unity ECS
Unity ECS is an integral part of Unity Data-Oriented Technology Stack (DOTS). Since its first public appearance in 2018, it was under heavy development and I believe it slowly progresses to a stable version.
A few weeks ago Unity released Project Tiny C#. Project Tiny is an initiative to minimize the footprint of the game engine to enable developers, create tiny web games with Unity. It is a very impressive technology and I was keen to explore it and maybe even build a few games with it. However, after doing some exploration, I felt the urge to write a few blog posts first 😀. So here I am, this is the first blog post.
In Unity ECS, a component which holds data needs to implement IDataComponent
an interface and has to be defined as a struct. It also has to follow this instruction (via Unity Docs):
IComponentData
structs may not contain references to managed objects
In other words, components need to be blittable — pure value type with fixed size and no pointers to other memory regions. If you need to define a component that holds a reference to a non fix size value (e.g. strings or a collection), you need to check out Dynamic Buffers. I might write another article which will concentrate on them, but not today.
How does one compute the size of a struct?
One might assume that the size of a struct is equal to the sum of the size of its properties. So:
struct A {
int Health;
}
instance of struct A
occupies 4 bytes as an int
occupies 4 bytes. And this is correct. ✅
What about:
struct B {
int Health;
bool IsDead;
}
an instance of a struct B
should occupy 5 bytes, because int
occupies 4 bytes and bool
occupies 1 byte, correct? And this is where we are wrong. 🛑
The instance of the struct B
occupies 8 bytes, thanks to a little thing called memory alignment.
WTF is Memory Alignment and why does it steal my memory?
Consider memory alignment as a necessary evil.
It is needed so that the CPU can process values continuously (word afterword).
To make my point clear, let me introduce another example:
struct S1 {
bool a;
int b;
}
If there were no memory alignment, an instance of S1
would have the following memory footprint:
[a|b|b|b|b]
We have 5 bytes, where the first byte is the value a
and second, to fifth bytes represent value b
. The problem is however that a CPU will not be able to read the value b
directly (for more information read this article). This is why an aligned version of struct S1 looks as following:
[
a|-|-|-|
b|b|b|b|
]
The bytes second till fourth are filled up with “empty space”, so that property b can be read directly from fifth to the eighth byte as one “word”.
The memory alignment is performed automatically, so we don’t need to bother with misaligned memory access. However, the order of fields might have a negative effect on the size of the struct instance, if we are not careful.
struct C {
bool IsDead; // a
int Health; // b
bool NeedsHelp; // c
int XP; // d
}
The memory footprint of a C
an instance in current field order looks as following:
[
a|-|-|-|
b|b|b|b|
c|-|-|-|
d|d|d|d|
]
The alignment of struct C is 4 bytes and the size of the struct is 16 bytes.
If you don’t believe me you can use the following expressions yourself:
UnsafeUtility.AlignOf<C>();
UnsafeUtility.SizeOf<C>()
If we would rearrange the fields in the struct like this:
struct C {
int Health; // a
int XP; // b
bool IsDead; // c
bool NeedsHelp; // d
}
The memory footprint of a C
an instance would look as follows:
[
a|a|a|a|
b|b|b|b|
c|d|-|-|
]
Reducing the size of the struct to 12 bytes and by that shaving off 25% of memory footprint.
4 bytes might look like a small gain and it is if the number of entities that carry this component is limited. But if it is not the case — 25% makes a big difference.
The problem can be amplified even further when we define nested structs:
struct CB {
bool a;
long b;
bool c;
}struct C {
CB cb;
int d;
bool e;
}
To understand the footprint of C
we can inline struct CB
into struct C
as following:
struct C {
bool a;
long b;
bool c;
int d;
bool e;
}
Which leads to memory footprint:
[
a|-|-|-|-|-|-|-|
b|b|b|b|b|b|b|b|
c|-|-|-|-|-|-|-|
d|d|d|d|e|-|-|-|
]
And this is 32 bytes. Where if we would arrange the fields in a better way:
struct C {
long b;
int d;
bool a;
bool c;
bool e;
}
We can reduce the memory footprint by 50%:
[
b|b|b|b|b|b|b|b|
d|d|d|d|a|c|e|-|
]
How should one avoid wasting memory based on alignment?
In best case scenario, we have a tool in Unity which will analyze our component structs and warn and tell us how the definition can be changed in order to avoid wasteful definitions. I might end up writing this tool myself at some point, who knows 💁♂️.
But till then, I have three suggestions for you:
- Sort the fields in the struct by the field value size (bigger first, smallest last) and not by “historical” reasons.
- Try to avoid complex struct nesting, as they make it much more difficult to identify and avoid memory waste.
- Try to keep the number of properties in one component struct to a minimum. This however is a more complex topic specifically in regards to Unity ECS and this is what my next article will be about 😉.
[UPDATE 17.07.2019]
There is another gotcha I found analyzing the components in project Tiny.
If you define an enum in C# as follows:
enum E {
a, b, c
}
The size type of the enum value defaults to int
and takes up 4 bytes which let us define over 4 billion cases. From my experience 1 byte and by that only 256 cases is more than enough. So here comes another suggestion:
4. Limit your enum definitions to occupy only 1 byte.
You can do it by defining an enum as following:
enum E: byte {
a, b, c
}
Thank you very much for reading my articles and stay tuned for the next ones.
As always I would appreciate a clap or two.