Chat
Ask me anything
Ithy Logo

C# Performance Deep Dive: Record vs. Class vs. Struct - Which Reigns Supreme?

Unpacking memory allocation, garbage collection, and benchmarks to reveal the fastest data types for your .NET applications.

csharp-record-class-struct-performance-7ymhxl5e

Essential Insights

  • Memory Matters Most: Performance differences primarily stem from memory allocation: struct and record struct (value types) usually live on the faster stack, while class and record class (reference types) reside on the heap, incurring garbage collection overhead.
  • Structs Lead on Speed (Usually): Due to stack allocation and minimal overhead, struct and especially record struct are often faster for small, frequently accessed data structures, minimizing garbage collection pressure.
  • Context is King: While record struct often benchmarks fastest, the optimal choice depends on data size, immutability needs, required features (like inheritance), and specific application bottlenecks. Don't choose based on speed alone.

Understanding the Contenders: Class, Struct, and Record

In C#, the way you define your data structures using class, struct, or record has significant implications for performance. These differences arise from their fundamental nature as either reference types or value types and how they are managed in memory.

Classes: The Reference Type Standard

Classes are the cornerstone of object-oriented programming in C#. They are reference types. This means when you create an instance of a class (an object), memory is allocated on the heap, and the variable holds only a reference (like a pointer or address) to that memory location.

Key Characteristics:

  • Heap Allocation: Allocating memory on the heap is generally slower than stack allocation.
  • Garbage Collection (GC): Objects on the heap are managed by the .NET Garbage Collector. While automatic memory management is convenient, the GC process can introduce pauses and overhead, potentially impacting performance, especially when many short-lived objects are created.
  • Reference Semantics: Assigning a class variable to another variable copies only the reference, not the object itself. Both variables point to the same object on the heap. Changes made through one variable are visible through the other.
  • Features: Classes support inheritance, polymorphism, and encapsulation, making them suitable for complex object hierarchies and behaviors.
  • Default Equality: By default, equality checks (==) compare references (identity equality), not the object's content.
Diagram illustrating Stack and Heap memory allocation in C#

Visual representation of Stack (for value types and references) and Heap (for reference type objects).

Structs: The Value Type Workhorse

Structs are value types. Variables of a struct type directly contain the data. Typically, structs allocated as local variables or method parameters reside on the stack, which is a highly efficient region of memory.

Key Characteristics:

  • Stack Allocation (Usually): Stack allocation is very fast because it involves simply adjusting a pointer. Memory is automatically reclaimed when the stack frame is popped (e.g., when a method returns), bypassing the need for garbage collection for the struct itself. (Note: If a struct is a field within a class, it's stored inline within the class object on the heap).
  • No GC Overhead (Directly): Since stack-allocated structs aren't managed by the GC, they avoid associated performance costs.
  • Value Semantics: Assigning a struct variable to another creates a full copy of the data. Each variable holds an independent instance. Changes to one copy do not affect the other. Passing structs to methods also typically involves copying the entire struct.
  • Performance: Generally faster for small data structures due to stack allocation and better cache locality. However, copying large structs can become expensive and negate performance benefits.
  • Limitations: Structs do not support implementation inheritance (they implicitly inherit from System.ValueType) and are generally intended for lightweight data representation.
  • Default Equality: By default, equality checks compare the values of all fields (memberwise equality).

Records: Modern Data Carriers

Introduced in C# 9 and enhanced in C# 10, records provide a concise syntax for defining types whose primary purpose is storing data. They emphasize immutability and value-based equality.

Key Characteristics:

  • Two Flavors:
    • record class (or just record): A reference type, allocated on the heap like a regular class, but with compiler-generated methods for immutability (using init-only setters by default), value-based equality (Equals, GetHashCode), and non-destructive mutation (with expressions).
    • record struct: A value type, allocated on the stack (like regular structs), offering the benefits of value types combined with the record features like concise syntax, immutability, and value-based equality.
  • Immutability Focus: Records are designed to be immutable by default, meaning their state cannot typically be changed after creation. This simplifies reasoning about state and enhances thread safety.
  • Value-Based Equality: Records automatically implement Equals and GetHashCode based on the values of their public properties, not their memory addresses. Two record instances are equal if all their corresponding properties are equal.
  • Conciseness: Provide a much shorter syntax for defining data-centric types compared to traditional classes or structs.

The Speed Showdown: Memory Allocation and Performance

The most significant factor determining the relative speed of classes, structs, and records is how and where they store their data in memory.

Stack vs. Heap: The Core Performance Difference

Memory in .NET is primarily managed in two areas: the stack and the heap.

  • The Stack: A LIFO (Last-In, First-Out) structure. Memory allocation/deallocation is extremely fast (just moving a pointer). It's used for storing value types (like int, bool, struct, record struct), method parameters, local variables, and references to heap objects. Data locality is excellent, which is good for CPU caches.
  • The Heap: A large pool of memory used for dynamic allocation of reference type objects (class, record class, arrays, strings). Allocation involves finding free space and is slower than stack allocation. Deallocation is handled by the Garbage Collector (GC), which introduces overhead. Accessing heap objects requires an extra level of indirection (following the reference), which can slightly impact performance and cache efficiency.

Because struct and record struct instances are often allocated on the stack, they avoid the overhead associated with heap allocation and garbage collection, making them inherently faster for creation and destruction, especially for short-lived, small objects.

Another diagram showing Stack and Heap

Illustration of how value types reside on the stack while reference types have a reference on the stack pointing to data on the heap.

Garbage Collection's Toll on Classes

The GC periodically scans the heap to find objects that are no longer referenced and reclaims their memory. While crucial for automatic memory management, GC cycles consume CPU time and can cause application pauses, especially during intensive allocation scenarios common with classes and record classes. Reducing heap allocations by using structs or record structs where appropriate is a key performance optimization strategy in C#.

The Copying Cost of Value Types

While stack allocation is fast, value types (struct, record struct) have a potential performance downside: they are copied by value. When you assign a struct to a new variable or pass it as a method argument (without ref or in modifiers), the entire data content is duplicated. For small structs (e.g., less than 16 bytes, though the exact threshold varies), this copying is usually negligible and outweighed by the benefits of stack allocation. However, for very large structs, the cost of copying can become significant and potentially make them slower than using a reference type (class) where only the reference is copied.


Benchmarking the Trio: What the Numbers Say

While theoretical differences point towards structs and record structs being faster, benchmarks provide concrete evidence. Numerous performance tests using tools like BenchmarkDotNet consistently show patterns:

  • Allocation Speed: Structs and record structs generally exhibit significantly faster allocation times and generate far less memory "garbage" compared to classes and record classes due to stack allocation.
  • Access Speed: Direct access to stack memory often results in slightly faster data retrieval for value types compared to the indirect heap access required for reference types.
  • Record Struct Advantage: Benchmarks often highlight record struct as potentially the fastest option, especially when value-based equality and immutability are needed. Some tests show record structs can be considerably faster (even up to 20x in specific scenarios like equality checks) than record class and sometimes even slightly faster than plain struct due to optimized compiler-generated code.

Relative Performance Characteristics

This chart provides a generalized, relative comparison of the performance characteristics. Higher scores indicate better performance or lower negative impact (e.g., higher GC Impact score means *less* negative impact). Note that these are relative and subjective scores for illustrative purposes; actual performance depends heavily on the specific workload.


Equality, Immutability, and Their Performance Impact

Value-Based Equality in Records

Records (record class and record struct) automatically provide implementations for value-based equality. This means two record instances are considered equal if their public properties have the same values. While convenient, performing value-based equality can be more computationally expensive than the default reference equality check used by classes (which just compares memory addresses). However, the compiler-generated equality methods for records are often highly optimized, especially for record struct, potentially outperforming manual implementations or reflection-based comparisons.

Immutability's Role

Records encourage immutability (data cannot be changed after creation). While not a direct speed boost in single-threaded scenarios, immutability simplifies program logic, especially in concurrent or multi-threaded applications. It eliminates entire classes of bugs related to shared mutable state and can reduce the need for defensive copying or locking, indirectly contributing to better overall system performance and robustness.


Visualizing the Concepts

This mind map summarizes the key distinctions and relationships between Classes, Structs, and Records in C# concerning their type, memory management, and core characteristics.

mindmap root["C# Type Performance Comparison"] id1["Class"] id1a["Reference Type"] id1b["Heap Allocation"] id1c["Garbage Collected"] id1d["Reference Semantics (Copy Ref)"] id1e["Supports Inheritance"] id1f["Default: Reference Equality"] id1g["Performance: Slower (Heap/GC)"] id2["Struct"] id2a["Value Type"] id2b["Stack Allocation (Typically)"] id2c["Not Directly GC'd (if on stack)"] id2d["Value Semantics (Copy Data)"] id2e["No Inheritance"] id2f["Default: Memberwise Equality"] id2g["Performance: Faster (Stack, less GC)
**BUT** Copy cost for large structs"] id3["Record"] id3a["Record Class"] id3aa["Reference Type"] id3ab["Heap Allocation"] id3ac["Garbage Collected"] id3ad["Reference Semantics"] id3ae["Immutability Focus"] id3af["Value-Based Equality (Compiler Gen)"] id3ag["Performance: Similar to Class,
maybe slightly better/worse
depending on equality usage"] id3b["Record Struct"] id3ba["Value Type"] id3bb["Stack Allocation (Typically)"] id3bc["Not Directly GC'd (if on stack)"] id3bd["Value Semantics"] id3be["Immutability Focus"] id3bf["Value-Based Equality (Compiler Gen, Optimized)"] id3bg["Performance: Often Fastest
(Stack + Optimized Features)
**BUT** Copy cost for large structs"]

Feature and Performance Summary Table

This table provides a concise summary of the key differences relevant to performance:

Feature Class Struct Record Class Record Struct
Type Kind Reference Type Value Type Reference Type Value Type
Memory Location Heap Stack (typically) / Inline Heap Stack (typically) / Inline
Allocation Overhead High (Heap + GC) Low (Stack) High (Heap + GC) Low (Stack)
Garbage Collection Impact Yes No (directly, if on stack) Yes No (directly, if on stack)
Assignment/Passing Copies Reference Copies Data Copies Reference Copies Data
Copying Cost Low (Reference) Potentially High (Large Structs) Low (Reference) Potentially High (Large Structs)
Default Equality Reference-Based Memberwise Value-Based Value-Based (Compiler-Generated) Value-Based (Compiler-Generated, Optimized)
Immutability Manual Implementation Manual Implementation Built-in Support (Default) Built-in Support (Default)
Inheritance Yes No (from other structs/classes) Yes (from other classes/record classes) No (from other structs/classes)
General Performance Rank (Fastest First) 4th 2nd 3rd 1st
Best Use Case (Performance Focus) Large, complex, mutable objects; Polymorphism needed Small, frequently used, immutable data; Minimize GC Immutable data objects where reference semantics are needed/acceptable Small, immutable data objects; Maximize performance & minimize GC; Value semantics needed

Video Explainer

For a visual and verbal explanation of the differences between these C# types, including when to use each, check out this helpful video:

This video covers the fundamental concepts of classes, structs, record classes, and record structs, providing context that complements the performance discussion.


Making the Right Choice: Performance vs. Design

While benchmarks often show record struct > struct > record class > class in terms of raw speed for allocation and access, choosing solely based on performance can lead to poor design. Consider these guidelines:

  • Use class or record class when:
    • You need reference semantics (multiple variables pointing to the same mutable object).
    • The object represents an entity with identity, not just data values.
    • You need inheritance or polymorphism.
    • The data structure is large, making copying expensive.
    • You prefer heap allocation and GC management for lifecycle.
  • Use struct or record struct when:
    • The type primarily represents a single value or small aggregation of data (like Point, Color, ComplexNumber).
    • Instances are small (generally under 16-32 bytes).
    • Instances are short-lived or created/destroyed frequently (to minimize GC pressure).
    • Value semantics (copying on assignment/passing) are desired.
    • Immutability is desired (record struct makes this easy).
    • You want to maximize performance in critical code paths by avoiding heap allocations.
  • Between struct and record struct: Choose record struct if you benefit from built-in immutability features and optimized value-based equality comparisons with a concise syntax. Choose struct if you need fine-grained control over mutability or don't need the record-specific features.
  • Between class and record class: Choose record class when you need a reference type primarily for carrying immutable data and want built-in value equality and with expression support. Choose class for traditional OOP scenarios involving behavior, mutable state, and identity.

Always measure! If performance is critical, use profiling tools and benchmarks like BenchmarkDotNet within your specific application context to validate your choices.


Frequently Asked Questions (FAQ)

► When should I *definitely* choose struct for performance?

► Is `record struct` always the absolute fastest?

► Does immutability in records impact performance?

► What happens if my struct becomes too large?


Recommended Reading


References


Last updated May 5, 2025
Ask Ithy AI
Download Article
Delete Article