Why You Should Avoid Runtime Instantiation and Destruction in Unity
The Unity Game Engine is centered around the concepts of Game Objects, that is a collection of components defining behaviour and recording data (inheriting from the MonoBehaviour class), and the process of creating a Game Object at runtime is called instantiating.
Now this seems to be a pretty basic operation, but it is usually better to avoid it at runtime, or at least hide it behind a loading screen, for several reasons.
Unity is NOT a C# Engine
This is what we refer to when we talk about managed and unmanaged code: some of the datatypes we manipulate in C# are actually references to C++ datatypes. This interop between the 2 can have a heavy computational cost and slow down your game loop extensively.
While there has been a lot of work done by the Unity team in recent years towards using C# as the only language for both scripting and the engine itself to avoid this interop cost and to allow for better control of vectorization of operations, this cost still exists and it better to avoid operations which might trigger it.
As you might expect, instantiating a GameObject is one such operation.
Another related problem is when a GameObject is destroyed: because of this dichotomy between managed and unmanaged code, calling Destroy will incur a heavy runtime penalty, and it will ONLY destroy the native C++ object because there is nothing to really destroy in the C# side. What happens in this case is that the managed part of the object will be marked as destroyed but it will actually remain in memory as long as there references to it, and Unity will be faking that the reference to this object is null.
This can be a significant trap: an object you believe to be destroyed might not have been destroyed and might be keeping a lot of memory for itself. It is good practice to set any reference to a destroyed object as null by hand to avoid this case.
Inefficient Garbage Collection
Garbage Collecting is the process by which objects and classes which are on the heap and are supposed to be destroyed are marked and the memory they used is freed. GameObjects which are created will be allocated on the heap. There are different algorithms to perform this, and Unity makes use of the Boehm algorithm.
The specifity of this algorithm is that it is what is called a “Stop the world algorithm”. This means this algorithm runs on the main thread and everything else is stopped while it runs. If you instantiate objects frequently, or create a lot of reference types, you’re creating garbage that must be cleaned by this algorithm. This WILL create what is called a GC spike when the GC actually tries to free up your memory: if you take a look at the profiler, this spike is quite obvious when it happens.
The other problem about it is that it is non-compacting, which means that it will not reorganize memory and the more you instantiate, the more likely it is that your system will need to allocate more memory to the heap and the size of the heap WILL NOT decrease with time. Now, this could potentially not be an issue (I personally prefer to be efficient with the available memory), except for the fact that this particular GC algorithm not only does not reallocate memory to close the gaps, but it is also non-generational, which means whenever the GC algorithm checks for objects to clean, it must perform a check on the whole heap. The bigger the heap, the longer it takes.
All of this is important to understand: if you’re not careful with your memory allocations, you’ll be faced with heap fragmentation (which could translate to Out of Memory errors and cache misses), and a Garbage Collection algorithm that blocks the execution of your game loop for an ever increasing period of time.
Be Smart about Instantiating
Overall, it is still okay to perform instantiation and destruction of GameObjects at runtime, but it’s better to be smart about it to avoid Garbage Collection (which can be exceedingly slow and doesn’t actually collect anything) and the costs of interop between managed and unmanaged code.
When possible it is better to preinstantiate the game objects in your scene, but you can also make use of some design patterns to avoid instantiating and destroying gameobjects, such as a pooling pattern, which we’ll explore in another article.