DOD — Unity Memory Best Practices — Part 6

Nitzan Wilnai
3 min readJun 6, 2024

--

Struct vs Class Arrays

In DOD, when grouping our data, it is better to use arrays of Struct objects rather than arrays of Class objects. The reason is that arrays of Structs are guaranteed to be allocated sequentially in memory and therefore have more of a chance to be on the same cache line.

For example, let’s say we have a struct and a class:

public Struct FooStruct
{
public int bar1;
public int bar2;
}

public Class FooClass
{
public int bar1;
public int bar2;
}

And we create arrays for both:

FooStruct[] m_structArray = new FooStruct[4];
FooClass[] m_classArray = new FooClass[4];

Both arrays will be allocated on the heap.

The size of m_structArray will be 32 bytes, but the size of m_classArray will be 16 bytes.

The reason is that m_structArray will include its data, while m_classArray will just be an array of address that point to the data.

m_structArray’s memory will look like this:

-----------------------------------
| 4 bytes | m_structArray[0].Bar1 |
-----------------------------------
| 4 bytes | m_structArray[0].Bar2 |
-----------------------------------
| 4 bytes | m_structArray[1].Bar1 |
-----------------------------------
| 4 bytes | m_structArray[1].Bar2 |
-----------------------------------
| 4 bytes | m_structArray[2].Bar1 |
-----------------------------------
| 4 bytes | m_structArray[2].Bar2 |
-----------------------------------
| 4 bytes | m_structArray[3].Bar1 |
-----------------------------------
| 4 bytes | m_structArray[3].Bar2 |
-----------------------------------

The memory includes the member variables Bar1 and Bar2 for each entry.

m_classArray, on the other hand, will be an array of addresses, each pointing to some location in memory where each object will be allocated:

-----------------------------------------------------
| 4 bytes | m_classArray[0] -> address of FooClass0 |
-----------------------------------------------------
| 4 bytes | m_classArray[1] -> address of FooClass1 |
-----------------------------------------------------
| 4 bytes | m_classArray[2] -> address of FooClass2 |
-----------------------------------------------------
| 4 bytes | m_classArray[3] -> address of FooClass3 |
-----------------------------------------------------

In fact, each member variable of m_classArray will need to be allocated separately:

for(int i = 0; i < m_classArray.Length; i++)
m_classArray[i] = new FooClass();

Each time we do m_classArray[i] = new FooClass(), a new FooClass object is allocated wherever the memory manager finds space. It’s entirely possible that the memory manager will scatter the objects around.

Between each object there might be other data, which will fill up the cache line:

------------------------------------
| 8 bytes | m_classArray[0] data |
------------------------------------
| ??? bytes | Some other data |
------------------------------------
| 8 bytes | m_classArray[1] data |
------------------------------------
| ??? bytes | Some other data |
------------------------------------
| 8 bytes | m_classArray[2] data |
------------------------------------
| ??? bytes | Some other data |
------------------------------------
| 8 bytes | m_classArray[3] data |
------------------------------------

That means accessing m_classArray[0] might not include the data from m_classArray[1] or m_classArray[2] in the same cache line.

If we keep our struct small and free of heap allocated objects, making an array of Struct objects instead of Class objects guarantees that they will be allocated sequentially in memory and will limit our cache misses.

Part 7 — https://medium.com/@nitzanwilnai/dod-unity-memory-best-practices-part-7-bbabd8f49fd3

--

--