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!