Back to TIL 2026-02-10

The "Cost" of Immutability in .NET

.NET Performance

In the .NET ecosystem, the "cost" of immutability is primarily measured in allocations and GC (Garbage Collection) pressure. While C# has added features like records and init-only properties to make immutability easier to write, it handles the underlying memory differently than "pure" functional languages.

Here is a high-level technical breakdown of how C# compares to Haskell, Elixir, Scala, and F#.


1. Structural Sharing vs. Defensive Copying

The biggest "cost" in C# is that it lacks native persistent data structures in the standard library (though System.Collections.Immutable exists).

2. Allocation and GC Pressure

C#'s Garbage Collector is a Generational Mark-and-Sweep collector. It is highly optimized for "short-lived" objects (Generation 0).

3. Comparison Table: C# vs. The Field

Feature C# (14) Haskell / Elixir F# / Scala
Default State Mutable Immutable (Strict) Immutable (Preferential)
Data Update with { } (Copy) Structural Sharing Structural Sharing
Memory Layout Contiguous (Fast cache) Linked/Tree (Cache misses) Mixed
Equality Reference (Default) Value/Structural Value/Structural
Threading Lock-heavy (if mutable) Lock-free (by design) Lock-free (mostly)

4. When the "Cost" becomes a "Profit"

It's a common misconception that immutability is always slower. In modern C#, immutability can actually increase performance in specific scenarios:

  1. Thread Safety: A mutable object requires lock statements or Monitor entries to be thread-safe. Locks are incredibly expensive (context switching, CPU stalls). An immutable record requires zero locks, often making it faster in high-concurrency web apps (like ASP.NET Core).
  2. Identity Cache: Since a pure, immutable object never changes, you can safely use it as a key in a Dictionary or cache its hash code. You never have to worry about the hash changing while it's in a collection.

Expert Strategy: The "Functional Core, Imperative Shell"

In C# 14, the most performant way to use FP is the sandwich approach:

  1. The Shell (Imperative): Use mutable logic for I/O, UI, and high-frequency loops where you need O(1) performance.
  2. The Core (Functional): Use records and IEnumerable for your business logic.

Warning: Avoid using System.Collections.Immutable in a tight for loop (e.g., adding 10,000 items to an ImmutableList). Because it lacks the specialized runtime optimizations of Haskell, it will be significantly slower than a mutable List<T> that you simply "freeze" or wrap in a ReadOnlyCollection at the end.