Another Way Structs Create Garbage
As Unity programmers, the garbage collector is sadly our enemy. C# struct
s 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, struct
s 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 struct
s 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:
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.
#1 by Barliesque on May 19th, 2016 ·
What happens, I wonder, if you use the Generic Struct with Default Constructor, but instantiate and assign like so:
var s = new GenericStruct { Val = 0 };
#2 by jackson on May 19th, 2016 ·
Since that is syntax sugar, it creates the same bytecode and doesn’t create any garbage. It’s a good alternative though, so thanks for pointing it out.
#3 by Sebastiano on February 14th, 2021 ·
Hi,
I stumbled upon this too. Is this a unity related issue or .net one? I find it very strange too, I am asking around.
#4 by jackson on February 14th, 2021 ·
This is a .NET issue, but the article demonstrates it with Unity.
#5 by sebastiano on February 15th, 2021 ·
I finally got to the bottom of the issue. In reality, this is by “design”. the CLR does allocate stuff for cache internally the VERY first time the structure with a new combination of parameters type is used. Unity GC test system, unfortunately, catches it as well, even though it is not a GC allocation (it is an internal CLR allocation, very likely native?). If the code is warmed up correctly, the allocation won’t happen.
BTW I wasn’t able to reproduce the problem with your specific simple scenario, but it was something similar in my more complex scenario and the unit test doesn’t fail once the code is warmed up correctly.
#6 by sebastiano on February 15th, 2021 ·
I realised my previous message may have suffered from some lack of context. This article of yours is a bit old and things changed in unity since 2016. This may be the reason why with unity 2020.2 I am not able to reproduce your scenario. My scenario was also relative to generic structs, but in hindsight, it may have been just a coincidence and the two cases may be unrelated.
Still the unity
fails if the code is not warmed up correctly and the CLR allocates stuff for its own benefit.
#7 by jackson on February 15th, 2021 ·
Thanks for posting your findings here! This is indeed “by design” rather than a bug. As you’ve found, each execution environment (e.g. IL2CPP) will implement the specifics of this allocation differently. There may well be some reusable cache to optimize common cases. It goes to show the importance of checking the details of your specific environment.