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!