Structs or Classes?

I am making a simulation of groups of people and I’m wondering whether to use structs or classes for these groups. Each group has a population, birth rate, and death rate which are floats and integers, however they also reference a Culture and Tech class. There are thousands of groups running at the same time and I’ve heard structs are more performant but I’m not sure if they fit my needs.

Class - reference type, that means it’s placed in heap (RAM or/and another memory, based on system specs on which program runs). Heap → Slow.

Struct - value type, that means it can be cached. Cached → CPU can access faster to the data. This is why ECS (DOTS) from Unity hugely use struct, because they are cache friendly, they less exposed to memory fragmentation, predictable for the loop vectorization by the compiler. In addition, it’s not tracked by GC, because no dynamic allocations to heap.

BUT! For example, if we use List, which stores structs, this will not work, because List is a dynamic data type, which performs allocations in heap, so, your structs (which are elements of List) will be heap allocated, so, whoops, not like array, which is stack allocated, but elements of List, like elements of an array, also linerly placed in memory, so, CPU makes less work.

This is good illustration what usually CPU does with structs (ECS’s table):

Important thing about structs is, you don’t share reference to the “real” instance, where with this reference, we can change state of that object, instead, we get copy.

NOTE: In Many Programming Languages exist shuffle copy and deep copy, deep copy is always happens when we deal with structs, but again, if struct has field, which stores reference (class), we don’t make new instance of a class, we just make copy of reference, because class’s default behaviour is to use shuffle copy.

Some words about struct’s semantics in C#:
Struct usage in C# is so not friendly, I prefer use class, for the compiler optimizations I use sealed class, it affects not only on compiled C#, but also on translated IL2CPP code. Also, if class used only as “data storage”, compiler can convert class to struct, and with sealed keyword, we’re marking for the compiler that it’s safe to do.

Classes are reference types and are stored on the heap, while structs are value types and can be stored on either the stack or the heap, depending on usage. Both types can lead to dynamic heap allocations, potentially adding pressure to the garbage collector.

Memory fragmentation is generally an issue for collections rather than individual structs. Loop vectorization depends on how your structs are implemented. So it’s not something that automatically gives you a performance advantage unless you know what you are doing.

Both classes and structs can be cached. Caching is unrelated to whether a type is a value or reference type. Concerns about heap speed are usually unwarranted, as heap usage is inevitable, and stack size is limited (e.g., 1MB for 32-bit apps, 4MB for 64-bit apps per thread). You will try to avoid heap allocations only in very specific, performance critical paths of you code, not in whole implementations of big game systems.

Structs can be faster or slower than classes based on implementation. Misusing structs:

  • Can be slower due to boxing/unboxing and cause GC allocations
  • Can be slower because of defensive copies created from the compiler

BUT: Everything above doesn’t matter, performance is not something that you should be concerned about in code that is not yet written. I doubt that the performance of structs vs classes in the scripting part of a game is something that creates notable differences unless we are talking about a game with at least a seven digits number budget and over 10 programmers working at it.

You should go with classes because they are easier to work with, you will be more productive, you will create less bugs and are easier to debug.

For example: Because structs are value types, you will never have a null reference exception in case you haven’t initialize something, your program will not behave as expected due to the way structs get initialized, structs always have an implicit parameterless constructor, so your structs are always initialized but with default values that can make your program have strange behavior until you realize that.

Or you may need to pass a struct by reference because it is too big or the performance will be worse than classes. Plus many other things, these were just two examples.

Choose classes for simplicity and maintainability. If performance is a significant concern, consider shifting to an Entity Component System (ECS) architecture rather than switching from classes to structs.

1 Like

Good opinion and fixes of my wrong information about caching.

And I agree with opinion that performance doesn’t matter in most cases, only in critical places.

1 Like

In C# you can’t allocate arrays on stack, it’s the same pointer to the heap memory. Well, you technically can, using stackalloc and spans, but it’s pretty limited, and in 99% of cases people use either Array/ListPool of some kind or NativeArray + Jobs for performance critical things.

1 Like

Oh, interesting, didn’t know about that. I’m not professional programmer, but I was using low-level languages, like C++ and Rust (mostly), I thought that the same behavior in C#, but nope. Thanks!

In C# Lists are a wrapper over arrays, they are essentially the same thing with the only difference that lists can change in size (by disposing the previous array, creating a new bigger array and copying the data to the new one).

As @Lekret mentioned above, both will be heap allocated.

Well, yes, I understood that array also is heap allocated, and, yes, I know that List is a dynamic array, aka vector.

No, lists are not vectors and are not dynamic arrays. In C#, the concept of a “dynamic array” does not exist.

Lists are arrays with the following distinction:

  • When a list reaches its capacity, it creates a new array with a larger size.
  • The data from the old array is copied into the new one, and the old array is eventually garbage collected.

In summary, arrays in C# cannot change size dynamically; they are fixed in size once created. Lists provide resizing functionality, but this is achieved by creating new arrays internally, not by modifying the size of the existing array, there are no dynamic arrays in C#.

1 Like

I actually learned about ECS after starting the project and I don’t want to have to rewrite everything. But I know that is is the exact system that I need. Is it possible to switch to ecs while keeping some stuff I already have?

Okay, interesting, thanks for invaluable information. Really helps better understand C#. I didn’t know about that.

If you never worked with ECS, especially with DOTS, then you shouldn’t rewrite everything and can just continue with your current approach. You can optimize performance critical things by using structs, Jobs etc.

ECS generally is not about performance, it’s about architecture flexibility, easier logic addition and changes. “Decent” level of performance by default is a nice bonus.
ECS != DOTS, DOTS is one of many other ECS implementations, but on steroids for maximum performance, and it comes with its own complexity and learning curve. If you want to learn it or any other 3rd party ECS library, I recommend you starting with fresh project and small scope e.g. Snake, Tic-Tac-Toe or Tetris in ECS.
Don’t rewrite your project unless you absolutely need to and know what you are doing.

1 Like

It is possible to implement only some systems using ECS. In a block-building game I worked on, we just re-implemented the voxel engine using ECS, and got a very substantial boost to framerates and reduction in battery drain. All dynamic entities on top of the voxel world were still implemented using Game Object Component architecture.

In fact, if you go to unity.com/ecs, this is the very first sentence they use to describe the framework:

ECS (Entity Component System) is a data-oriented framework compatible with GameObjects.

While the entity component system architecture pattern in general is not just about performance, the reason why ECS for Unity was introduced to the Unity ecosystem was pretty much all because of performance. Unity’s ECS implementation focuses heavily on data-oriented design, and is part of Unity’s data-oriented tech stack (DOTS). And data-oriented design itself is all about optimization.

Here’s a good intro to data-oriented design and DOTS from Jason Booth, that might be helpful to get you started:

In it he gets a 3.5x boost to performance (6ms → 1.7ms) for his system just from switching from using classes to using structs and data-oriented design.

And this was all done without affecting the public API of the MonoBehaviour in any way. it’s all just implementation details, encapsulated inside the class, and the component can continue to be used in a project using GameObject Component architecture just like before.

1 Like

That video appears to be unavailable.

1 Like

Link fixed. Thanks for the heads up!

2 Likes