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.8 µs ±431 ns | 20.2 µs ±30 ns | 1.00× | — | -3% |
| Foreach_loop | 19.6 µs ±369 ns | 19.2 µs ±166 ns | 0.95× | — | -2% |
| Linq_sum | 33.6 µs ±552 ns | 26.7 µs ±25 ns | 1.32× | — | -21% |
| Span_loop | 16.3 µs ±186 ns | 16.7 µs ±313 ns | 0.82× | — | +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.0× faster than the baseline.
In this run: CountBy_linq is fastest — 3.0× faster than the baseline.
RESULTS
| method | net9.0 | net10.0 | ratio | allocated | Δ net10.0 |
|---|---|---|---|---|---|
| GroupBy_count baseline | 324.5 µs ±1.4 µs | 304.3 µs ±1.8 µs | 1.00× | 323.0 KB | -6% |
| CountBy_linq | 130.8 µs ±764 ns | 102.4 µs ±783 ns | 0.34× | 47.1 KB | -22% |
| Manual_dictionary | 180.0 µs ±835 ns | 152.3 µs ±639 ns | 0.50× | 46.9 KB | -15% |
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