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.
Essential Insights
- Memory Matters Most: Performance differences primarily stem from memory allocation:
structandrecord struct(value types) usually live on the faster stack, whileclassandrecord class(reference types) reside on the heap, incurring garbage collection overhead. - Structs Lead on Speed (Usually): Due to stack allocation and minimal overhead,
structand especiallyrecord structare often faster for small, frequently accessed data structures, minimizing garbage collection pressure. - Context is King: While
record structoften 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.
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 justrecord): A reference type, allocated on the heap like a regular class, but with compiler-generated methods for immutability (usinginit-only setters by default), value-based equality (Equals,GetHashCode), and non-destructive mutation (withexpressions).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
EqualsandGetHashCodebased 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.
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 structas 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) thanrecord classand sometimes even slightly faster than plainstructdue 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.
**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
classorrecord classwhen:- 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
structorrecord structwhen:- 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 structmakes this easy). - You want to maximize performance in critical code paths by avoiding heap allocations.
- The type primarily represents a single value or small aggregation of data (like
- Between
structandrecord struct: Chooserecord structif you benefit from built-in immutability features and optimized value-based equality comparisons with a concise syntax. Choosestructif you need fine-grained control over mutability or don't need the record-specific features. - Between
classandrecord class: Chooserecord classwhen you need a reference type primarily for carrying immutable data and want built-in value equality andwithexpression support. Chooseclassfor 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?
Use structs when you have small (e.g., representing coordinates, colors, complex numbers, or small data packets), frequently created/destroyed data structures where minimizing garbage collection pressure is paramount. High-performance loops, game development physics calculations, or parsing large amounts of small data pieces are prime candidates where the overhead of heap allocation and GC for classes would be detrimental. Ensure the struct size remains small to avoid expensive copying.
► Is `record struct` always the absolute fastest?
Often, yes, particularly in scenarios involving creation, destruction, and value-based equality checks, due to stack allocation and compiler optimizations. However, "fastest" depends on the operation. If you have a very large `record struct`, the cost of copying it when passing by value might make a `record class` (where only the reference is copied) perform better in specific contexts. Benchmarking your specific use case is crucial.
► Does immutability in records impact performance?
Directly, immutability itself doesn't make single operations faster or slower in a simple single-threaded context. Its performance benefit is often indirect. By ensuring data doesn't change, it simplifies reasoning, reduces bugs in concurrent scenarios (less need for locks), and allows for safe sharing without defensive copying. "Updates" via `with` expressions create new instances, which involves allocation, but the overall system design might become simpler and more robust, potentially leading to better performance characteristics in complex applications.
► What happens if my struct becomes too large?
If a struct becomes large (general guidelines often suggest reconsidering above 16 or 32 bytes, but it depends heavily on usage patterns), the cost of copying it during assignments or method calls can outweigh the benefits of stack allocation. Frequent copying of large structs can lead to performance degradation. In such cases, refactoring to a `class` or `record class` (which are passed by reference) might be more efficient, despite the heap allocation and GC overhead.
Recommended Reading
- Explore detailed benchmark comparisons of C# record struct vs struct vs class allocation performance.
- Find out when struct copying overhead typically becomes a performance bottleneck in C#.
- Learn more about the impact of garbage collection on the performance of C# applications.
- Discover how to use 'in' and 'ref' modifiers to optimize passing structs in C# and avoid copying.
References
Last updated May 5, 2025