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:

  1. Value Type Semantics (struct): Instances are allocated on the stack or inline within containing types, avoiding heap allocations and reducing Garbage Collection (GC) overhead.
  2. Record Semantics (record): The compiler automatically synthesizes value-based equality, hash code generation, formatting methods (ToString), and non-destructive mutation via with expressions.
  3. 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.