Optimizing Code by Replacing Classes with Structs, Unity C# Tutorial
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; }
}
}
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