Using Structs to Avoid Creating Garbage
It’s easy to forget about struct
in C#. After all, it’s not available in other languages like Java or AS3 and it seems to have fewer features than good old class
. But struct
can really help you out when it comes to garbage creation! Today’s article discusses some strategies to get the most out of struct
. Read on to learn how to use structs to put a stop to that pesky garbage collector!
It helps to keep in mind how struct
and class
are allocated in memory. When you use new
with a class
, the memory to hold its fields is allocated from the heap area of RAM. The garbage collector begins tracking this memory. When there are no more references to the class
instance, the garbage collector considers it garbage and may kick in to free up the heap memory usage. That collection process can be extremely slow and cause major framerate hiccups in your game.
In contrast, when you use new
with a struct
the memory is allocated on the stack area of RAM. The stack is limited, temporary memory that’s used to store all the local variables in all the functions of the call stack. All “value types” in C# are allocated from the stack: int
, float
, bool
, etc. Allocating here is super fast because the stack just grows linearly as more functions are called. Each function call allocates room for all the local variables it uses, including any struct
instances. The garbage collector isn’t involved in any way. It doesn’t track stack memory and doesn’t collect it. From a GC point of view, the stack is invisible.
So struct
never causes any garbage collection? Not so fast. There is a big difference between struct
instances as local variables and struct
instances as fields of a class
. While local variables are always on the stack, class
instances are always allocated on the heap. When a class
instance is allocated on the heap, memory is reserved for all of its fields. When a field is a struct
, all of those fields will be reserved. This is in contrast to a class
instance as a field. In that case the only memory reserved on the heap is a tiny (typically 8 bytes) pointer/reference to the location in the heap where the class
has been allocated.
This means that struct
is not a magic bullet. If you switch all of the fields of your classes from class
instances to struct
instances, the amount of memory your class takes up in the heap will grow tremendously. You can still do this, but it’s best to keep in mind just how much memory your struct
will take up. A Vector3
is a tiny struct of just three float
so it’s no big deal. A Player
that you’ve converted from class
to struct
might be a kilobyte or more!
Another important fact to keep in mind is that struct
instances are copied with each assignment or passing to a function. Unlike with class
, you’re not just copying a pointer/reference to a single instance but actually making a full, deep copy of the struct
instance. Not only does this take longer than copying a tiny pointer/reference but it also means that you have to allocate enough memory for the struct
instance you’re copying from and the struct
instance you’re copying to because you’ll have two in memory at the same time. Again, this isn’t a big deal for Vector3
instances, but you should keep it in mind before you make any huge struct
types or start copying them around a lot.
There is one exception to this copying that C# provides as a sort of workaround. You can override the normal “by value” parameter passing of struct
instances to a function. This is done using the ref
keyword, like so:
struct HugeStruct { int Id; // ... 10 KB of more fields } void Test() { var hs = new HugeStruct(); hs.Id = 100; ByValue(hs); // ... Id still 100 ByReference(ref hs); // ... Id now 123 } void ByValue(HugeStruct hs) { hs.Id = 123; } void ByReference(ref HugeStruct hs) { hs.Id = 123; }
When ByValue
is called a full copy of HugeStruct
is made: all 10 KB of its fields. This is slow and eats into the precious, limited stack space. You might only get a few hundred KB total, so be careful with a huge struct
like this. Additionally, changes made to the parameter aren’t reflected in the local variable that Test
has because ByValue
is just changing its local copy.
In contrast, when ByReference
is called only a pointer/reference to the local variable is passed. This is typically only 8 bytes, so it’s much quicker than copying a whole 10 KB object. This means that when ByReference
makes changes to the parameter, it’s changing the same memory that holds the local variable in Test
. So when the call to ByReference
returns the local variable will have its contents changed. Don’t worry about this accidentally happening though- you have to explicitly accept this risk by using the ref
keyword when you declare and call the function.
In summary, there is great potential in struct
due to the fact that it can be allocated on the stack rather than the heap where it would be tracked by the garbage collector. Just make sure to keep the size of the struct
small, pass it by reference when it’s sizable or you want to change the caller’s version of it, and to be wary of storing too much struct
data as class
fields. If you do all three of these, you can avoid the garbage collector’s wrath.
Got any thoughts about struct
? Do you use it or avoid it in your own games? Share your thoughts in the comments!
#1 by Martin Hauschild on February 16th, 2017 ·
Hey there,
I found a tiny mistake in the code, the function is called “ByReference” but you call it as “ByRef”.
Your blog is super awesome, very insightful!
Thanks for all of it :)
#2 by jackson on February 16th, 2017 ·
I’m glad you’re enjoying the articles! Thanks for letting me know about this typo. I’ve updated the article with a fix.
#3 by alexs on May 17th, 2018 ·
Cool, but all wrong )
https://blogs.msdn.microsoft.com/ericlippert/2010/09/30/the-truth-about-value-types/
#4 by jackson on May 17th, 2018 ·
Thanks for the link; that’s an excellent article. I don’t think my article is “all wrong” though. Can you elaborate on a specific part of it that you consider wrong?
#5 by Fernando on February 29th, 2020 ·
Hi. Lately I´ve had some parts in my code that repeat themselves but with different variables. I thought it would be good to create a separate void that does this once and just pass different variables to it; but since passing parameters only creates a copy of them it would be generating garbage. I don´t know much about how the garbage collector works or how much bytes everything adds. I more or less know about it, but honestly not enough to understand everything in this article. Anyway, do you think something like this would avoid garbage and be, you know, not expensive in any way?
//am just asking because am repeating this kind of operations a lot and is using too much space (not memory space or anything: it´s just visually large and hard to read).
#6 by jackson on February 29th, 2020 ·
Using
ref
,out
, orin
parameters won’t cause any garbage to be created and should generally run very fast. If you find it helps you to clean up your code, then it should be fine to use them.#7 by markip on November 25th, 2021 ·
Could you elaborate :
“This is in contrast to a class instance as a field. In that case the only memory reserved on the heap is a tiny (typically 8 bytes) pointer/reference to the location in the heap where the class has been allocated.”
I don’t understand the difference it makes in terms of memory usage. All the memory necessary to hold the field is still allocated on the heap, albeit in a different location.
#8 by jackson on November 25th, 2021 ·
The only difference it makes in terms of memory usage is the extra 8 bytes to store the pointer (a memory address) in addition to the class instance. That may be an issue for cache utilization since 8 bytes is quite significant when you’re trying to process a lot of struct or class instances.
The bigger issue for this article is that there are now two class instances to be allocated and garbage-collected. Another issue is that accessing the memory via that pointer probably entails reading further cache lines and cache misses can be very expensive in performance-critical code.