·C#·2 min read·Mid-level developers

LINQ Performance in .NET 9 / 10 — Where the JIT Helps You and Where It Doesn't

The runtime quietly made LINQ a lot faster. The patterns that still cost you are the ones the JIT can't fix on your behalf.

There's a perennial blog post that goes "LINQ is slow, use for loops." It hasn't aged well. .NET 9 (and 10) made huge swathes of LINQ effectively free. The patterns that still hurt are the ones the JIT can't see through.

Here's the cheat sheet I keep open:

The runtime fixed these:

  • arr.Count() on a known-length collection — folds to arr.Length.
  • Take(0), Skip(huge), Distinct().Count() short-circuit.
  • Sum, Average, Min, Max over int[] and double[] are vectorised (SIMD).
  • Select(...).First() doesn't enumerate everything anymore.

The runtime can't fix these. They're on you:

  • Multiple enumeration. var q = items.Where(...); and then iterating q twice means the predicate runs twice. Call .ToList() once or use foreach.
  • Closures capturing locals. items.Where(x => x.Id == id) allocates a closure for id. In a hot path, hoist it or use a static lambda with state.
  • ToList() in a hot loop. Allocates a new list per iteration. If you only need to iterate, drop the ToList(). If you need a buffer, use ArrayPool<T>.
  • OrderBy() in a hot path. Quicksort allocates and isn't free. If the list is small and bounded, a manual sort or Span<T>.Sort() wins by an order of magnitude.

Quick benchmark on a 10k-int array, summed:

Pattern Mean Allocated
arr.Sum() (.NET 9, SIMD) 1.8 µs 0 B
Hand-written for loop 1.7 µs 0 B
arr.Where(x => x > 0).Sum() 24 µs 40 B
Same, with static lambda 22 µs 0 B

.Sum() is now within rounding error of the manual loop. The cost shows up the moment you add a delegate. That's the actual rule: pay for Where/Select delegates when you need readability, save them for hot loops where you can prove the cost.

The only "always" advice: stop turning IEnumerable into List when you don't need to. Half the LINQ bills I've seen at scale were .ToList().Where(...) instead of just .Where(...). Removing the extra allocation is free and the next person who reads the code won't have to wonder why it was there.