When programming games one common design aspect any developer must keep in mind is that of memory management, since if forgotten or done improperly it will slow down your game and ruin the experience for your players. With this is mind the Unity development team has constructed their engine to operate using Managed Languages which essentially means that a Garbage Collection service is provided to help clean up the managed heap once full. Understanding the Garbage Collector will lead any developer towards a more machine friendly way to structure their code base and reduce the need to go back and optimize, or rewrite, entire code sections later in development. While this can seem like a daunting concept to fully understand, and believe me it is, this article will focus on just the aspects of Garbage Collection and Memory Management that concerns Unity developers.
When attempting to understand memory allocation it’s first important to understand the two different types that are allocated to memory, that being Reference types and Value types. Value types are primitive values such as int float or even structs, these are placed into Stack Memory which is faster because it’s a stack data structure. Reference types are basically references to objects such as strings, arrays, or classes, these are placed into Heap Memory which is slow to manage due to it being allocated using a heap data structure that requires multiple memory address pointers. Both the Heap and Stack are allocated in Unity using Virtual Memory Space, meaning a block of memory addresses that is utilized solely for your game.
Unity utilizes a Managed Heap to allocate game objects and various reference types, this Managed Heap is what the Garbage Collector monitors and reclaims in order to remove objects that fall out of scope, meaning their object is no longer referenced by your game. It should also be noted that the Managed Heap only ever grows in size doing so whenever an allocation is made that won’t fit inside the current heap allocation. Growing the heap is a very time expensive process requiring a new heap being allocated and copying all references, in their proper node order. This should be avoided by making sure your heap is large enough at start up to handle all required game objects. One such way to accomplish this is to perform all game object instantiations at start up and maintain them with a pool data structure. This method has the added benefit of keeping the heap from becoming fragmented which would slow down your game.
While the Heap is managed by Unity it’s important to note that Stack Memory is only managed based on scope. Once an object or function in unity loses scope, meaning it is no longer being referenced within the current scope, the stack memory that scope allocated is reclaimed immediately and all value types are deallocated while any reference types remain in the heap until a garbage collection is triggered. The stack is a separate portion of the virtual memory Unity allocates and is controlled by a single memory address pointer which is offset by the total allocations made at the top of the stack. This structure is what makes stack memory so fast to allocate and reallocate, since by it’s very definition it’s a last in first out data structure.
Garbage Collection Basics
With the introduction of IL2CPP Unity utilizes a Boehm–Demers–Weiser garbage collector algorithm. This was included with the current build of Mono without generational sorting of references, which is why Unity’s Heap fragmentation is such an issue for long game sessions. The essence of the algorithm walks each reference node searching for nodes that lack parent nodes, after which they are marked for deletion. Parent nodes include local variables on the stack or static variables since both these have long scope durations. The Unity road map, as of writing this post, contains plans to eventually migrate to a generational garbage collection algorithm however no date is set.
If you include a reference to an object or string inside a struct Unity’s garbage collector will be forced to inspect the structs reference on a collection.
According to Microsoft the rules for the use of struct are as follows:
- It logically represents a single value, similar to primitive types (
- It has an instance size under 16 bytes.
- It is immutable.
- It will not have to be boxed frequently.
- Microsoft’s value type sizes
It’s important to understand what type of data you’re using and where you allocate it, in general all value types begin with lower case letters, with the exception being strings, and all reference types should begin with upper case letters.
A note on using value types is that when passed into methods or functions they are copied in the stack, to avoid this attempt to keep their scope within the class as much as possible and read them from within functions. So attempt to only ever pass reference types as parameters in functions.
When working with strings it’s often common practice to concatenate them together, this is a very bad practice because strings are immutable in C-Sharp. Instead opt for the StringBuilder class or avoid concatenation altogether.
For each loops are bad to utilize in the main update loop since they result in an enumerator reference being allocated and deallocated which results in garbage quickly accumulating.
Avoid allocating memory within for loops, instead allocate any variables outside the loop then utilize them as the loop iterates.
Accessing properties can result in an allocation occurring, such as is the case with Vector3.zero and Quaternion.identity, it’s suggested to store and get such values from a constant value class.
For more best practices here are some articles released by the Unity development team and Microsoft, they contain several code examples that demonstrate ways to structure games to both avoid garbage or schedule regular collections. Finally it should be noted that the best practice for writing any piece of software is always to first make it work, then make it work better.