Overview

2 Structuring data for performance

In this chapter, the main idea is that performance in Unity depends not just on using arrays, but on how data is shaped, stored, and moved through memory. The author explains that choosing between structs and classes, understanding stack versus heap allocation, and being careful about data locality can all have a major impact on speed, cache efficiency, and memory behavior. These choices matter broadly, even though the examples focus on C# and Unity.

Structs are best used for small, fixed-size value data that is usually used together, such as a two-component position, while classes are better for larger, longer-lived data or data made up of arrays and strings. The chapter emphasizes that structs can be efficient because they often fit neatly on the stack and are easy to allocate and discard, but they can also be copied unintentionally when passed to functions. By contrast, classes live on the heap, so passing them around copies only the reference, not the full data, which avoids expensive copying but can make memory locality less predictable.

The chapter also warns that structs are not always “stack-only,” since structs inside classes or containing arrays and strings still rely on heap-allocated memory for those members. Arrays of structs tend to be contiguous and cache-friendly, while arrays of classes store references that point to separate heap objects and can hurt locality. Finally, the author shows that primitive data type selection matters too: smaller types can save memory, but on some devices they may actually be slower than int, so the safest approach is to choose types based on the maximum required range, then profile on target hardware before optimizing.

A simplified view of the stack before, during, and after calling CountEnemiesInRange()
The stack before calling EnemyInRange(), inside enemyInRange() but before calling distance(), inside distance(), after exiting distance(), and after exiting EnemyInRange().
A task for adding experience to our game. To implement it, we’d need to store the current player’s experience in game data.
When myGameDataStruct is passed by value into AddXP(), its contents are copied into gameDataStruct. When we modify gameDataStruct.XP we are modifying the copy, so myGameDataStruct.XP’s value remains the same.
When myGameDataStruct is passed by reference, both myGameDataStruct and gameDataStruct point to the same memory address. Modifying one of them also modifies the other, so changing gameDataStruct.XP also changes myGameDataStruct.XP.
Our EnemyData struct is on the stack, but the arrays point to memory on the heap, and each array points to a different heap memory location.
The variable EnemyData.Name is on the stack, but it points to heap memory where the string “Enemy Name” is stored.
When we change the value of Enemy.Name, it points to a new heap memory location. The old string “Enemy Name” will remain in memory until it is freed by the garbage collector.
When we collect a coin that was dropped by an enemy, we want the coin to fly to the UI’s top bar, where we show the player's money.
Arrays of structs like CoinStructArray are allocated in a single, contiguous block of heap memory, which holds all their data.
Arrays of classes like CoinClassArray are composed of addresses, and are initially set to null.
Arrays of class objects are addresses that point to wherever each object was allocated on the heap.
If we add a string CoinName to our CoinStructArray, each string will need to be allocated separately and its data will no longer be in contiguous memory, breaking our data locality.
This figure shows two cache lines, one filled with ints and one with bytes, and shows that we can fit 4x more byte data than int data in a cache line.

Summary

  • You should use C# classes when storing variable-length data, like arrays and strings, and data that is not always used together.
  • You should use C# structs only when your data is always used together and does not exceed 16 bytes in size.
  • Data is pushed onto the stack in a Last-In-First-Out order.
  • The stack helps with data locality because it stores data in contiguous memory.
  • Stack is also useful with memory management, as stack data is automatically deallocated when it goes out of scope.
  • The heap is used to store long-lived data.
  • Heap data is allocated where there is room, so data can end up spread out, leading to cache misses.
  • C# structs are passed by value by default, which can lead to bugs if you modify the copy instead of the original.
  • The larger the struct, the bigger the performance hit we’ll experience in the Unity editor. This can lead to a performance disparity between what we see in the editor and the device, making performance analysis difficult.
  • Copying by ref is useful because only the address is copied to the stack, so the performance is the same regardless of the size of the source data.
  • C# structs are typically stored on the stack, unless they are a member variable of a heap allocated object.
  • C# struct member variables of variable length, like strings or arrays, will need to be allocated on the heap even if the struct was allocated on the stack.
  • C# arrays of structs will be stored in a single contiguous block, which helps with data locality.
  • C# arrays of class only hold an address to each object, which is stored wherever there is room on the heap, which breaks data locality.
  • Saving space by using a byte or short instead of an int can have serious performance implications, depending on the target device.

FAQ

When should I use a struct instead of a class in Unity C#?Use a struct when the data is small, composed of primitive value data, used together as a unit, and likely to be temporary. A good rule of thumb is to keep structs around 16 bytes or less. Use a class when the data is long-lived, variable in size, or not always used together.
Why is a class often better for holding arrays of game data?A class is better when it holds arrays because the class object lives on the heap, while the arrays themselves are also heap data. This fits long-lived game data well, such as enemy databases or configuration data used across multiple frames.
What is the difference between stack memory and heap memory?The stack is fast, limited, and organized in last-in-first-out order, making it ideal for temporary local data. The heap is larger and used for long-lived or dynamically allocated data, but it is less predictable for data locality and can contribute to fragmentation and garbage collection overhead.
Why are structs generally faster for temporary data?Structs are often efficient for temporary data because they can be stored on the stack, which is fast to allocate and deallocate. Their lifetime is tied to the scope of the function, so they can be automatically reclaimed when the function ends.
What happens when I pass a struct into a function?By default, a struct is passed by value, so its contents are copied into the function. If the struct is large, this copy can hurt performance and can also lead to bugs if you accidentally modify the copy instead of the original.
How do I modify a struct inside a function without copying it?Use the ref keyword when passing the struct. This passes a reference to the original data, so changes made inside the function affect the original struct instead of a copy.
Why is the 16-byte struct guideline important?It is a practical rule of thumb because many CPUs can move 16 bytes efficiently, often using registers. Structs larger than that may become more expensive to copy by value, and performance can vary between the Unity Editor, Mono, and IL2CPP builds.
Why are arrays of structs usually better than arrays of classes for performance?Arrays of structs are stored contiguously in memory, which improves data locality and cache usage. Arrays of classes store references, and each object is allocated separately on the heap, making memory layout less predictable and more prone to cache misses.
Why should I avoid strings and arrays inside structs when performance matters?Because strings and arrays are heap-allocated reference data. Even if the struct itself is on the stack, those members still point to heap memory, which reduces the performance advantage of using a struct and can increase allocation and garbage collection pressure.
Should I always use the smallest possible numeric type, like byte or short?Not necessarily. Smaller types can save memory and bandwidth, but they may be slower on some devices. For runtime gameplay data, int is usually the safest default unless profiling on your target hardware shows a smaller type is better or storage size matters more than raw speed.

pro $24.99 per month

  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose one free eBook per month to keep
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime

lite $19.99 per month

  • access to all Manning books, including MEAPs!

team

5, 10 or 20 seats+ for your team - learn more


choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • High Performance Unity Game Development ebook for free
choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • High Performance Unity Game Development ebook for free