As Unity programmers, the garbage collector is sadly our enemy. C# structs are often a great tool to avoid allocating objects that need to later be garbage-collected. This isn’t always the case though. Sometimes even a struct can allocate garbage. Today’s article points out one of those ways so you won’t be fooled into thinking you’ve stopped the GC just because you’re using a struct. Read on to learn more!

Normally, structs are allocated on the stack rather than the heap and therefore get popped off like int and float variables. The garbage collector isn’t involved, so everything runs nice and fast with no fear of extra memory usage or fragmentation. Here’s an example where everything works out just fine:

struct Point2
{
	float X;
	float Y;
}
 
Point2 Add(Point2 a, Point2 b)
{
	var p = new Point2(); // <-- created on stack
	p.X = a.X + b.X;
	p.Y = a.Y + b.Y;
	return p; // <-- popped off stack
}

When we use new to create a Point2 we’re not doing the same thing that we do when we use new with a class. The object is being created on the stack like if we were to declare a local int variable. The p variable isn’t allocated on the heap and therefore the garbage collector isn’t responsible for tracking how many times its referenced or cleaning it up later on.

We can add functions to Point2 and the same remains true. For example, here’s a constructor to simply things:

struct Point2
{
	float X;
	float Y;
 
	Point2(float x, float y)
	{
		X = x;
		Y = y;
	}
}
 
Point2 Add(Point2 a, Point2 b)
{
	return new Point2(a.X + b.X, a.Y + b.Y); // <-- created on and popped off stack
}

Adding and using this constructor doesn’t change anything about the memory picture. The Point2 is still created on the stack rather than the heap and the GC still doesn’t touch it.

Likewise, we can also add a type parameter to Point2 using C#’s generics system. This allows us to make a Point2<int> or a Point2<double> in case we need different component types:

struct Point2<TComponent>
{
	TComponent X;
	TComponent Y;
}
 
Point2<float> Add(Point2<float> a, Point2<float> b)
{
	var p = new Point2<float>(); // <-- created on stack
	p.X = a.X + b.X;
	p.Y = a.Y + b.Y;
	return p; // <-- popped off stack
}
 
// note: more overloads for other types (e.g. double) go here

This still doesn’t change anything memory-wise. Despite the flexibility we’ve gained from abstracting the component types, we still have a struct that we can put on the stack to avoid the GC.

Strangely, we can’t mix these two features without allocating garbage. Here’s a version that uses generics and a constructor function:

struct Point2<TComponent>
{
	TComponent X;
	TComponent Y;
 
	Point2(TComponent x, TComponent y)
	{
		X = x;
		Y = y;
	}
}
 
Point2<float> Add(Point2<float> a, Point2<float> b)
{
	return new Point2(a.X + b.X, a.Y + b.Y); // <-- created on heap, garbage-collected!
}
 
// note: more overloads for other types (e.g. double) go here

It’s strange that this combination of features would result in garbage creation, so let’s prove this claim with a test script. Here’s one that has two structs that have non-default constructors. One is a generic struct and the other not. The script will create one of each type using the default constructor and one of each type using the non-default constructor. This is done in individual functions to make analysis in the Unity Profiler easier. Here’s the test:

using UnityEngine;
 
public class TestScript : MonoBehaviour
{
	struct NormalStruct
	{
		public int Val;
 
		public NormalStruct(int val)
		{
			Val = val;
		}
	}
 
	struct GenericStruct<T>
	{
		public T Val;
 
		public GenericStruct(T val)
		{
			Val = val;
		}
	}
 
	void Start()
	{
		TestNormalStructDefaultCtor();
		TestNormalStructParamsCtor();
		TestGenericStructDefaultCtor();
		TestGenericStructParamsCtor();
	}
 
	void TestNormalStructDefaultCtor()
	{
		var s = new NormalStruct();
		s.Val = 0;
	}
 
	void TestNormalStructParamsCtor()
	{
		new NormalStruct(0);
	}
 
	void TestGenericStructDefaultCtor()
	{
		var s = new GenericStruct<int>();
		s.Val = 0;
	}
 
	void TestGenericStructParamsCtor()
	{
		new GenericStruct<int>(0);
	}
}

And here’s the result in the Unity 5.3.4f1 Profiler using Deep mode:

Struct GC Alloc Profiler

Notice that neither “normal” non-generic struct creates any bytes in the “GC Alloc” column. The generic struct also has zero bytes in the “GC Alloc” column, but only when using the default constructor that takes no parameters. When using the non-default constructor that does take parameters, the generic struct suddenly starts allocating 32 bytes of garbage.

While 32 bytes is surely small, it’s quite easy to accidentally create lots of little allocations every frame that add up over seconds or minutes of gameplay. Eventually the GC will run, potentially with disastrous results such as framerate loss or memory fragmentation. It’s much better to eliminate the GC allocation in the first place. Luckily that’s easy to do. You just need to use the default constructor or a helper function, so long as it isn’t actually a constructor:

static class GenericStructUtils
{
	public static GenericStruct<T> Create<T>(T val)
	{
		var s = new GenericStruct<T>();
		s.Val = val;
		return s;
	}
}
 
// bad, creates garbage
var s = new GenericStruct<int>(0);
 
// OK, uses stack, duplicates code, verbose
var s = new GenericStruct<int>();
s.Val = 0;
 
// good, uses stack, reuses code, concise
var s = GenericStructUtils.Create(0);

Hopefully this is helpful to you in the fight against the GC. Let me know in the comments if you’ve got any tips or tricks on how to cut down on garbage creation.