Benchmark LINQ Against Manual Loops Before Assuming Either Is Faster
Benchmark LINQ Against Manual Loops Before Assuming Either Is Faster
The Standard
Do not assume LINQ is "slow" or that a manual loop is "fast" — measure with BenchmarkDotNet against the actual data shape and operation in question before optimizing, and use Span<T>/CollectionsMarshal.AsSpan for indexed hot-path loops over List<T> rather than guessing which style is faster.
Why
The reference benchmarks compare List<int>.Max() and a lazy IEnumerable<int>.Max() against manual indexed loops and Span<T>-based loops, and compare a SelectMany().GroupBy().OrderByDescending().Take() LINQ pipeline against a hand-rolled Dictionary + Array.Sort alternative for a realistic "top-10 by frequency" operation — always with LINQ as the [Benchmark(Baseline = true)] so every alternative is measured relative to it. No results file or numeric conclusion ships with the code; the shape of the two approaches (an allocation-heavy GroupBy/OrderByDescending chain vs. a single-pass Dictionary + Array.Sort) is there to be measured, not assumed. Treat any "X is faster" claim as something to verify with a benchmark against your own data volumes, not as a fixed rule to apply blindly.
Before (Anti-pattern)
// Assuming LINQ is too slow and hand-rolling without measuring first
var counts = new Dictionary<int, int>();
foreach (var value in data)
{
foreach (var block in ToBlocks(value))
counts[block] = counts.TryGetValue(block, out var c) ? c + 1 : 1;
}
int[] candidates = counts.Keys.ToArray();
Array.Sort(candidates, (a, b) => counts[b].CompareTo(counts[a]));
int[] winners = candidates[..10];After (Standard)
[Benchmark(Baseline = true)]
public int[] Linq() =>
_data
.SelectMany(ToBlocks)
.GroupBy(block => block, (block, items) => (block, count: items.Count()))
.OrderByDescending(x => x.count)
.Select(x => x.block)
.Take(10)
.ToArray();
[Benchmark]
public int[] ManualDictionary()
{
var counts = new Dictionary<int, int>();
foreach (var value in _data)
foreach (var block in ToBlocks(value))
counts[block] = counts.TryGetValue(block, out var c) ? c + 1 : 1;
var candidates = counts.Keys.ToArray();
Array.Sort(candidates, (a, b) => counts[b].CompareTo(counts[a]));
return candidates[..10];
}
// Run both under BenchmarkDotNet against realistic data volumes before choosing.Rules for LLMs / Agents
- Do not rewrite a LINQ pipeline into a manual loop (or vice versa) for "performance" without a BenchmarkDotNet (or equivalent) measurement on representative data sizes.
- When a hot path genuinely needs to avoid LINQ's iterator/allocation overhead over a
List<T>, prefer an indexed loop orCollectionsMarshal.AsSpan(list)over converting to an array first. - Structure comparative benchmarks with the current/idiomatic approach as
[Benchmark(Baseline = true)]so alternatives are reported as a relative ratio, not raw numbers alone. - Do not cite specific speedup percentages in code comments or documentation unless a benchmark result backing that number is checked in alongside it.
When NOT to apply
For code that is not on a measured hot path, prefer the more readable LINQ pipeline by default — only trade readability for a manual loop where a benchmark shows it matters.