Garbage Gotchas
Sometimes it seems like Unity programming is a minefield. Plenty of innocuous-looking code secretly creates garbage and eventually the GC runs and causes a frame hitch. Today’s article is about some of those less-obvious ways to create garbage.
Polymorphic Parameters
Normal parameter passing is fine. You won’t have any problem passing a T
to a function that takes a T
. Likewise, normal polymorphism is fine. Feel free to assign a Cat
to a variable of type Animal
.
The problem arises only in particular cases. For example, int
has two overloads of Equals
:
bool Equals(int other); bool Equals(object other);
These can be called with all kinds of values. Check out this innocuous-looking code:
123.Equals(123); 123.Equals(123u);
The first call is to the first overload and the second call is to the second overload because it’s passing a uint
which is not exactly an int
. The compiler opts for the more general object
. That means the second call results in boxing the uint
parameter into an class that holds the value. Doing this allows it to be passed as an object
since all class instances derive from object
. Instantiating the class means creating garbage, 20 bytes in this case.
Similar issues come up in other specific cases. For example, if you pass a List<T>
to a function and it uses a foreach
loop on it then no garbage will be created. If, however, you follow OOP “best practices” and pass an IEnumerator<T>
then the same foreach
loop will create 40 bytes of garbage.
Var Args
Many functions are designed to take an arbitrary number of arguments. For example, Unity’s Mathf.Max
has two overloads:
float Max(float a, float b); float Max(params float[] values);
So say we call it like this:
Mathf.Max(123f, 123f); Mathf.Max(123f, 123f, 123f, 123f);
Again the first call goes to the first overload and the second call to the second overload. It sure doesn’t look like we did anything bad here, but we actually called a version that takes a float[]
. Since arrays are reference types in C#, an array needs to be created. Behind the scenes the compiler generates this:
Mathf.Max(new float[] { 123f, 123f, 123f, 123f });
Consequently, creating this temporary array results in 48 bytes of GC alloc. A workaround is to cache our own float[]
, fill in the values, then call the function with the array. It’s more code to type, awkward to keep track of the float[]
, and often we need different numbers of parameters so the cached array isn’t usable, but it will allow us to reuse the array.
Properties That Allocate
Every time we access UnityEngine.Object.name
it creates a new string
. Creating that string
creates garbage since string
is a class
. There’s also garbage for the internal array of char
that make up the string. For a single-character name
, 28 bytes of garbage are created. Add two bytes for each additional character.
This is unfortunate in three ways. First, it’s common to get the name of an object (e.g. GameObject
) and, for example, compare to see if it’s the one we’re looking for. Second, there’s no way to get Unity to cache this string
. It simply creates a new one every time. Third, it’s hard to make our own cache of the name. About the best we could do would be to add a NameComponent
that holds the cached name. Unfortunately that results in even more garbage creation since now we need to create one of those!
GameObject.tag
is a little better. Just like with name
, it creates a new string
every time. On the bright side, Unity has provided us with the CompareTag
function to handle the most common case: looking for game objects with some tag. Using this function doesn’t create any garbage.
No Initial Capacity
It’s perhaps unfortunate that the default constructor for .NET container types is to have zero initial capacity. So if we forget to put a capacity then we get the worst performance because the very first element we add will result in allocating some internal capacity to hold it. For example:
List<int> list = new List<int>(); list.Add(123);
The first line creates the List<int>
itself, which is 32 bytes of garbage. The list now holds an int[0]
array. When we call Add
it needs to immediately replace that array with one that can hold the newly-added item. So it grows to its internal (and undocumented) minimum of an int[4]
array. This creates 48 bytes of more garbage.
Now let’s look at what happens if we add a thousand items:
List<int> list = new List<int>(); for (int i = 0; i < 1000; ++i) { list.Add(123); }
This code creates 8.3 KB of garbage! We only needed 4000 bytes to hold the 1000 4-byte int
values, so where’d the rest of it come from? Some is overhead for the List<int>
like its internal Count
variable. Some is overhead for the int[]
itself like its Length
variable. Mostly, this was caused by continuously hitting the capacity limit and needing to resize. This is done by doubling the capacity, so we end up with the following arrays getting created:
4 // initial minimum 8 16 32 64 128 256 512 1024
In total, we caused eight extra arrays to be created and released to the GC. They had capacity for 1020 elements totaling 4080 bytes of memory, plus overhead. The final array we ended up at has capacity for 24 extra elements which uses 96 extra bytes of memory.
Now let’s look at what would have happened if we hadn’t used the default constructor for List<int>
. We could have specified the capacity we needed at the start:
List<int> list = new List<int>(1000); for (int i = 0; i < 1000; ++i) { list.Add(123); }
This code only creates 4 KB of garbage. Unity’s profiler isn’t specific about exact byte counts, but we should expect at least 1000 4-byte values: 4000 bytes. Since 4 KB is 4096 bytes and we expect some overhead, this is pretty much optimal.
By specifying the initial capacity we saved creating eight extra arrays, filling them with zeroes, copying from one to another, and about 4 KB of garbage creation. So if we know how big our list is going to be, or even have a decent guess, it’s usually well worth setting a non-zero initial capacity.
Remember: this applies to other collections like Dictionary
!
Logging
Debug.Log
is an absolute pig. A single log is enormously expensive compared to even pretty big lists like the above 1000-element one. Consider logging an empty string:
void Start() { One(); Two(); } void One() { Debug.Log(""); } void Two() { Debug.Log(""); }
The first log takes some of the overhead of starting up the Debug
class and initializing its static members, so it’s more expensive than subsequent logs. I’ve arranged the two calls into One
and Two
so it’s easier to see in the profiler how much garbage each of them creates. Ready for the totals? The first log creates 11.7 KB and the second creates 8.1 KB! Most of this is due to Unity building the stack trace that’s automatically appended to every log.
Given this large expense, it’s important to think about when we really need to be logging. Thankfully, there are a few alternatives. One simple one is to change Debug.logger.filterLogType
or Debug.logger.logEnabled
to strip out non-essential logs, especially in production builds. Another one is to make our own logging function and put the [Conditional]
attribute on it. Then we can set compile symbols such that all the calls to that function will be removed at compile time when those compile symbols aren’t set. For example, we might use [Conditional("LOG_DEBUG_ENABLED")] public static void Log(object message)
and simply not set “LOG_DEBUG_ENABLED” in production builds to strip out all the calls to this function.
Conclusion
These are just five ways that you can inadvertently create garbage while writing C# code for Unity. There are many more! If you want to avoid GC spikes, keep watching the profiler’s “GC Alloc” column and keep looking into why it’s not zero. Sometimes the answer will surprise you!
#1 by d3Eds on May 24th, 2019 ·
“Sometimes it seems like Unity programming is a minefield.”
Only sometimes?
#2 by Oliver on January 15th, 2024 ·
This post is so woefully misguided. 60% of your complaints are not even related to Unity so why are you saying that “Unity programming is a minefield”?
1. For your point about Equals, if you just use the == operator like you’re supposed to. This is a non-issue. The narrower operand is casted to the wider one and no boxing occurs. This is C# and .NET behaviour, not Unity.
2. Var args. This is well established, params allocates an array and it’s mentioned on the docs: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/method-parameters#params-modifier I don’t see what the big deal is. This is also C# and .NET, not Unity.
3. Your point about some Unity properties allocating (specifically the properties returning string) is actually the only decent point on here. Yes you should use CompareTag where possible but in situations where you need the tag (or the name) you can also just cache it in advance to avoid repeated allocs.
4. I genuinely don’t understand… point 3 complains about the allocations and garbage. You think a non-zero default capacity is going to minimise that? You literally just complained about garbage, and now you’re saying you want garbage. What if I only ever want 2 or 3 elements in my list? Why should it assume what capacity I want? The whole point of a List, by the way, is that it resizes itself based on its elements. If you have a specific size you need to maintain, just use an array. This is – again – a non-issue. And surprise, it’s also not a Unity issue.
5. Debug.Log is certainly a beast and that is why it’s in a class called Debug. It’s literally for debugging purposes. If you’re concerned about the comparatively tiny allocations that Log creates while you’re developing the game in the editor, your priorities are wrong.