Optimizing Code by Replacing Classes with Structs, Unity C# Tutorial

SwiftRoll šŸ¦
5 min readOct 31, 2023

--

If you donā€™t find yourself understanding the difference between structs and classes, I would recommend researching that first.
Otherwise, itā€™s possible to do more harm than good, because they work very
differently.
Hereā€™s a link about structs at Microsoft documentation
https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/struct

This tutorial is intentionally short, focusing on practical implementation rather than theory.
However, I encourage you to go deeper into these topics for more efficient code without much of additional work.

Allocation

Letā€™s begin with simple code that demonstrates performance comparison between creating classes and structs.
Weā€™ll create 1 million of each:

public sealed class Example: MonoBehaviour
{
private ClassData[] _classArray;
private StructData[] _structArray;

private void Start()
{
Profiler.BeginSample("Class creation"); //for performance measurements
_classArray = new ClassData[1000000];
for (int i = 0; i < _classArray.Length; i++)
_classArray[i] = new ClassData();
Profiler.EndSample(); //for performance measurements

Profiler.BeginSample("Struct creation"); //for performance measurements
_structArray = new StructData[1000000];
for (int i = 0; i < _structArray.Length; i++)
_structArray[i] = new StructData();
Profiler.EndSample(); //for performance measurements
}

private sealed class ClassData
{
public int Value { get; }
}

private readonly struct StructData
{
public int Value { get; }
}
}
Unity editor profiler screenshot

Letā€™s compare:
Class
- 26.7mb allocation
- 40.58ms
Struct
- 3.8mb allocation (~7x less)
- 1.88ms (~21x faster)

These numbers will vary for different types of data, but we can clearly see that structs perform better here.

Why?
Each new instance of class requires
allocation inside managed memory, which has some performance cost.
Also any
class instances require more memory than structs and have to be aligned inside managed memory.

Additionally, itā€™s important to remember that each class instance is individually allocated within the heap and participates in the Garbage Collection cycle. This effect on performance doesnā€™t end at allocation, it will affect GC collection later. And this is actually even more important reason to consider structs.

Usage

Iterating

Letā€™s add code that iterates through all the created objects

private void Update()
{
Profiler.BeginSample("Iterate Classes"); //for performance measurements
IterateClasses();
Profiler.EndSample(); //for performance measurements

Profiler.BeginSample("Iterate Structs"); //for performance measurements
IterateStructs();
Profiler.EndSample(); //for performance measurements
}

private void IterateClasses()
{
int sum = 0;
for (int i = 0; i < _classArray.Length; i++)
sum += _classArray[i].Value;
}

private void IterateStructs()
{
int sum = 0;
for (int i = 0; i < _structArray.Length; i++)
sum += _structArray[i].Value;
}

Classes
- 6.58ms
Structs
- 4.28ms (~1.5x faster)

Why?
ā€œ
Data localityā€ of elements inside the array.
If itā€™s an
array (or collection) of structs, then the data in memory is located directly inside the array and goes sequentially, and that increases ā€œcache hitsā€.
Opposite to that,
array (or collection) of classes contains only references for instances and these instances may be scattered randomly in memory so the performance will vary from case to case.
Also thereā€™s underlying
dereferencing to access class instances.

Passing

Thereā€™s 3 ways we can pass these values to methods:

  • Class
  • Struct
  • Struct by reference (ref/in/out keywords)

Weā€™ll try all of them and measure:

private int _sum;

private void Update()
{
ClassData classData = new();
StructData structData = new();

Profiler.BeginSample("Pass class");
for (int i = 0; i < 1000000; i++)
PassClass(classData);
Profiler.EndSample();

Profiler.BeginSample("Pass struct");
for (int i = 0; i < 1000000; i++)
PassStruct(structData);
Profiler.EndSample();

Profiler.BeginSample("Pass struct by ref");
for (int i = 0; i < 1000000; i++)
PassStructByRef(in structData);
Profiler.EndSample();
}

private void PassClass(ClassData data) => _sum += data.Value;
private void PassStruct(StructData data) => _sum += data.Value;
private void PassStructByRef(in StructData data) => _sum += data.Value;

Passing struct by reference is fastest in this scenario.

Now letā€™s modify the data to take more memory:

private sealed class ClassData
{
public int Value { get; }
public int Value1 { get; }
public int Value2 { get; }
public int Value3 { get; }
public int Value4 { get; }
}

private readonly struct StructData
{
public int Value { get; }
public int Value1 { get; }
public int Value2 { get; }
public int Value3 { get; }
public int Value4 { get; }
}

Passing directly by structs becomes heaviest because more data means more time to copy it.
But passing struct by reference still has best performance.

Consider passing a struct by reference when itā€™s possible, especially if itā€™s not very small struct memory-wise.

Advices

  • Most likely you wonā€™t need mutable structs, prefer using readonly structs unless you know what youā€™re doing
  • Consider using structs in performance-heavy parts of your code and when thereā€™s long collections of not too heavy data
  • Donā€™t make too heavy structs. If the objects takes a lot of memory, then itā€™s usually better to make it a class, otherwise copying will decrease performance
  • When working with structs, avoid ā€œboxing.ā€ If you find the need to ā€œboxā€ something, consider using a class instead.

Also check out my tutorial about Avoiding Mistakes When Using Structs in C#
https://medium.com/@swiftroll3d/avoiding-mistakes-when-using-structs-in-c-b1c23043fce0

What I used for measurement
Laptop with AMD Ryzen 9 5900HX CPU

Additional materials
- Microsoft guide to decide between class and struct https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/choosing-between-class-and-struct
- Microsoft docs optimisation 1
https://learn.microsoft.com/en-us/dotnet/csharp/advanced-topics/performance/
- Microsoft docs optimisation 2
https://learn.microsoft.com/en-us/dotnet/csharp/advanced-topics/performance/ref-tutorial
- Book ā€œPro .NET Memory Managementā€
https://prodotnetmemory.com/

Consider following me for more tutorials here or at twitter https://twitter.com/SwiftRollDev

--

--