Readonly Record Structs in C# for Maximum Performance
This article explores readonly record struct types
introduced in C# 10, detailing how they combine the memory efficiency of
value types with the compiler-enforced safety of immutable records. You
will learn the mechanics behind their zero-allocation profile, how the
readonly modifier eliminates hidden defensive copying, and
how the synthesized value equality avoids boxing to deliver optimal
runtime performance in modern .NET applications.
What Is a Readonly Record Struct?
A readonly record struct combines three distinct C#
capabilities into a single type declaration:
- Value Type Semantics (
struct): Instances are allocated on the stack or inline within containing types, avoiding heap allocations and reducing Garbage Collection (GC) overhead. - Record Semantics (
record): The compiler automatically synthesizes value-based equality, hash code generation, formatting methods (ToString), and non-destructive mutation viawithexpressions. - Immutability Enforcement (
readonly): All instance fields and properties are enforced as immutable at compile time.
public readonly record struct GeoCoordinate(double Latitude, double Longitude);Using positional syntax, the compiler generates
init-only properties and a primary constructor, preventing
state modifications after instantiation.
Eliminating Defensive Copies
The primary performance hazard of standard mutable structs is the
"defensive copy." When passing a struct by reference using the
in parameter modifier or storing it in a
ref readonly variable, the C# compiler must ensure that the
struct cannot be mutated by invoked members.
If a struct is not declared as readonly, the compiler
cannot verify that calling a property or method will not modify internal
state. Consequently, it creates a temporary defensive copy of the struct
in memory before executing the member call.
By applying the readonly modifier to the record struct,
you guarantee that no member can mutate internal state. The compiler
detects this guarantee and completely eliminates defensive copies,
enabling safe pass-by-reference operations with zero performance
penalty.
// Passed by reference with no heap allocation and no defensive copying
public double CalculateDistance(in GeoCoordinate start, in GeoCoordinate end)
{
double dLat = end.Latitude - start.Latitude;
double dLon = end.Longitude - start.Longitude;
return Math.Sqrt(dLat * dLat + dLon * dLon);
}Efficient Value Equality Without Boxing
Standard C# structs inherit equality checks from
System.ValueType. The default implementation uses
reflection and runtime type inspection to compare fields, which triggers
boxing and incurs significant CPU overhead.
A record struct automatically implements
IEquatable<T> and overrides Equals and
GetHashCode at compile time. The generated code directly
compares fields using strongly-typed operations:
- No reflection is performed at runtime.
- No boxing occurs when evaluating structural equality.
- Equality operators (
==and!=) are automatically overloaded and optimized.
Non-Destructive Mutation with Zero Heap Allocation
Immutability often introduces overhead when new states must be
derived from existing objects. In class-based records, using the
with expression allocates a new object on the managed
heap.
With a readonly record struct, non-destructive mutation
is performed entirely via stack-to-stack copy:
var pointA = new GeoCoordinate(37.7749, -122.4194);
var pointB = pointA with { Latitude = 37.7750 };This operation simply copies the underlying bits while modifying the targeted field, executing in sub-nanosecond time without engaging the garbage collector.
When to Use Readonly Record Structs
To achieve maximum performance, apply
readonly record struct under the following conditions:
- Small Data Size: The type contains 16 to 32 bytes of data (roughly 2 to 4 primitive fields).
- High-Throughput Scenarios: The data is processed frequently in tight loops, messaging pipelines, or game loops where GC pressure degrades throughput.
- Mathematical and Domain Primitives: Ideal for coordinates, complex numbers, money types, ranges, and small Data Transfer Objects (DTOs).
When the data size exceeds 32 bytes or requires extensive
inheritance, a standard record class remains preferable to
avoid the overhead of copying large memory blocks across method
boundaries.