LINQ, from the profiler to the compiler
Four mistakes found in production, the benchmarks, and what the compiler really generates
LINQ is declarative, but execution is imperative: the same syntax hides radically different costs depending on the data structure underneath. The series starts from a real audit on a fleet dispatcher with a latency budget under 100ms, puts numbers on it with BenchmarkDotNet, and goes down to the state machine the compiler generates.
The same code, two orders of magnitude apart
.Contains() on a List is O(n). .Contains() on a HashSet is O(1). The code you write is identical. The profiler tells two different stories.
That is the thread running through the whole series: LINQ is declarative, but execution is imperative, and the same syntax hides radically different costs depending on the data structure underneath. The starting point is a real audit, on a dispatcher for a commercial vehicle fleet that keeps everything in memory to stay under 100ms of latency: four patterns found in production, and the most expensive one was doing 1,400,000 comparisons where 2000 would have been enough.
From there the series goes down: the numbers measured with BenchmarkDotNet instead of estimated, the state machine the compiler generates behind a Where(), and finally how to make execution visible with tracing instead of having to read it off the code.
What you will learn
- ✓ Recognize the four patterns that turn linear operations into quadratic ones
- ✓ Measure with BenchmarkDotNet instead of guessing where the cost sits
- ✓ Understand what the compiler generates behind a Where(), and why it changes the bill
- ✓ Make execution visible with tracing, instead of reading it off the code
Articles in the series
- 014 LINQ Mistakes I Found in Production (And How They Cost 1000x) 17 min
Four common LINQ patterns that turn linear operations into quadratic ones. Analysis of real cases from a commercial fleet dispatch service on .NET 8.
→ - 02What Does LINQ Actually Cost? Real Benchmarks on .NET 8 14 min
Real benchmarks with BenchmarkDotNet on .NET 8: List vs HashSet, repeated scans vs GroupBy, LINQ allocations vs foreach, and the hidden cost of AsParallel on small collections.
→ - 03What the Compiler Generates When You Write a Where() — State Machines, Iterators, and IL 14 min
Behind every .Where() lives a compiler-generated state machine. In .NET 8 Enumerable.Where uses specialized iterators, and a cast turns them off.
→ - 04Seeing LINQ in Action: Tracing with OpenTelemetry and Grafana Tempo 15 min
Make LINQ pipeline execution visible with OpenTelemetry and Grafana Tempo. Extension methods for tracing stages, multiple enumeration, and explosive nesting on a live dashboard.
→