.NET struct performance degradation by size

I benchmarked equality, GetHashCode, and HashSet operations across different C# type implementations. The tests compared struct, readonly struct, record, and readonly record struct at various sizes (16-128 bytes).

Why?

The conventional wisdom has been “don’t have more than 16 bytes in a struct” with 32 bit CPU architectures, and no more than 32 bytes on 64 bit CPUs, but mostly the former. I wanted to see what the actual performance degradation was as struct size increases.

I like to use readonly record struct to model aspects of my domain knowing that the compiler will optimize the type away to just the properties it contains, while guaranteeing  domain correctness.

Setup

Tests were run on an Apple M3 using .NET 8 with BenchmarkDotNet’s ShortRun configuration.

  • 16 bytes: 4 int properties
  • 32 bytes: 8 int properties
  • 64 bytes: 16 int properties
  • 128 bytes: 32 int properties

record is a value type, so increasing the number of properties doesn’t change the performance characteristics much, but I did it to keep the basis for comparison as even as possible.

Equality Performance

Type 16-byte (ns) 32-byte (ns) 64-byte (ns) 128-byte (ns)
Struct 7.66 8.06 10.83 16.46
Struct + IEquatable 0.00 1.25 3.61 9.82
Readonly Struct 7.56 8.02 10.91 16.51
Readonly Struct + IEquatable 0.00 1.08 3.21 9.64
Readonly Record Struct 0.00 1.00 2.82 7.57
Record 0.94 1.27 2.55 6.32

GetHashCode Performance

Type 16-byte (ns) 32-byte (ns) 64-byte (ns) 128-byte (ns)
Struct 12.41 12.64 10.88 13.13
Struct + IEquatable 1.90 3.14 9.45 17.71
Readonly Struct 11.14 12.95 10.98 13.22
Readonly Struct + IEquatable 1.73 2.87 8.96 16.82
Readonly Record Struct 0.00 0.43 2.67 9.80
Record 1.33 1.79 4.31 10.82

HashSet.Contains Performance

Type 16-byte (ns) 32-byte (ns) 64-byte (ns) 128-byte (ns)
Struct 20.27 23.59 24.00 34.56
Struct + IEquatable 5.08 6.73 15.90 27.34
Readonly Struct 20.47 23.87 24.08 33.87
Readonly Struct + IEquatable 5.11 6.61 15.50 27.41
Readonly Record Struct 2.42 4.61 9.57 20.65
Record 6.14 6.89 10.51 19.20

Memory Allocations

Type 16-byte 32-byte 64-byte 128-byte
Struct (Equals) 64 B 96 B 160 B 288 B
Struct (GetHashCode) 32 B 48 B 80 B 144 B
Struct (HashSet) 96 B 144 B 240 B 432 B
Readonly Struct (Equals) 64 B 96 B 160 B 288 B
Readonly Struct (GetHashCode) 32 B 48 B 80 B 144 B
Readonly Struct (HashSet) 96 B 144 B 240 B 432 B
All others 0 B 0 B 0 B 0 B

Key Findings

  • Structs without IEquatable<T> use ValueType.Equals, which boxes and allocates,  causing 7-16ns overhead per operation.
  • Records consistently outperform structs for GetHashCode, likely due to compiler-generated optimizations.
  • Readonly record structs combine the benefits of value semantics with record optimizations, showing best overall performance for collection operations.
  • Struct size impacts performance linearly – 128-byte types take 3-5x longer than 16-byte types for most operations.

Recommendations

  • Always implement IEquatable<T> on structs. ValueType.Equals is 3-5x slower.
  • Use readonly record structs for small value types that need equality operations and collection membership tests.
  • Consider regular structs with IEquatable<T> for types larger than 64 bytes where mutability is needed.
  • Records (reference types) remain competitive for GetHashCode but have allocation overhead not shown in these benchmarks.

Leave a Reply

Your email address will not be published. Required fields are marked *