BENCHMARKS · LINQ & LOOPS
The eternal for/foreach/LINQ argument, plus GroupBy vs CountBy. Suite and raw reports: github.com/gdhami-net/dotnet-benchmarks.
SumTotals
Sum 10,000 order totals: for vs foreach vs LINQ vs a span loop — the eternal argument, measured.
TAKEAWAY Loops and LINQ are not rivals on cold paths - but in a hot loop, LINQ's delegate per element costs real time. Extract to an array/span when a loop shows up in a profile.
In this run: Span_loop is fastest — 1.2× faster than the baseline.
In this run: Span_loop is fastest — 1.2× faster than the baseline.
RESULTS
| method | net9.0 | net10.0 | ratio | allocated | Δ net10.0 |
|---|---|---|---|---|---|
| For_loop baseline | 20.1 µs ±660 ns | 20.2 µs ±36 ns | 1.00× | — | +1% |
| Foreach_loop | 18.9 µs ±107 ns | 19.3 µs ±318 ns | 0.96× | — | +3% |
| Linq_sum | 32.7 µs ±132 ns | 27.4 µs ±36 ns | 1.35× | — | -16% |
| Span_loop | 16.1 µs ±13 ns | 16.4 µs ±357 ns | 0.81× | — | +2% |
RATIO VS BASELINE · net10.0
THE LEDGER · SAME WORKLOAD ACROSS RELEASES (LOG)
For_loopForeach_loopLinq_sumSpan_loop
THE CODE BEING MEASURED
For_loop — what this measures
Classic indexed for loop.
[Benchmark (Baseline)]
public decimal For_loop()
{
decimal sum = 0;
for (var i = 0; i < _orders.Length; i++) sum += _orders[i].Total;
return sum;
}Foreach_loop — what this measures
foreach over the array.
[Benchmark]
public decimal Foreach_loop()
{
decimal sum = 0;
foreach (var o in _orders) sum += o.Total;
return sum;
}Linq_sum — what this measures
LINQ Sum with a selector.
[Benchmark]
public decimal Linq_sum() => _orders.Sum(o
=> o.Total);Span_loop — what this measures
foreach over a Span of pre-extracted totals.
[Benchmark]
public decimal Span_loop()
{
decimal sum = 0;
foreach (var t in _totals.AsSpan()) sum += t;
return sum;
}CountPerCustomer
Count orders per customer: GroupBy vs .NET 9's CountBy vs a manual dictionary.
TAKEAWAY GroupBy materializes every group just to count them. CountBy (or a manual dictionary of counters) does one pass with a fraction of the allocations.
In this run: CountBy_linq is fastest — 3.2× faster than the baseline.
In this run: CountBy_linq is fastest — 3.2× faster than the baseline.
RESULTS
| method | net9.0 | net10.0 | ratio | allocated | Δ net10.0 |
|---|---|---|---|---|---|
| GroupBy_count baseline | 352.8 µs ±30.1 µs | 343.2 µs ±24.2 µs | 1.00× | 323.0 KB | -3% |
| CountBy_linq | 130.3 µs ±838 ns | 106.0 µs ±1.9 µs | 0.31× | 47.1 KB | -19% |
| Manual_dictionary | 179.4 µs ±1.5 µs | 154.5 µs ±3.3 µs | 0.45× | 46.9 KB | -14% |
RATIO VS BASELINE · net10.0
THE LEDGER · SAME WORKLOAD ACROSS RELEASES (LOG)
GroupBy_countCountBy_linqManual_dictionary
THE CODE BEING MEASURED
GroupBy_count — what this measures
GroupBy then Count — the idiom everyone reaches for.
[Benchmark (Baseline)]
public int GroupBy_count() =>
_orders.GroupBy(o => o.Customer).Count(g
=> g.Count() > 10);CountBy_linq — what this measures
CountBy (.NET 9+): one pass, no groups materialized.
[Benchmark]
public int CountBy_linq() =>
_orders.CountBy(o => o.Customer).Count(kv
=> kv.Value > 10);Manual_dictionary — what this measures
A manual dictionary of counters.
[Benchmark]
public int Manual_dictionary()
{
var counts = new Dictionary<string, int>();
foreach (var o in _orders)
{
counts.TryGetValue(o.Customer, out var c);
counts[o.Customer] = c + 1;
}
var over = 0;
foreach (var kv in counts)
if (kv.Value > 10) over++;
return over;
}speed vs allocation
Fast is one axis. What it costs the GC is the other.
For_loopForeach_loopLinq_sumSpan_loopGroupBy_countCountBy_linqManual_dictionary