BENCHMARKS · LINQ & LOOPS

The eternal for/foreach/LINQ argument, plus GroupBy vs CountBy. Suite and raw reports: github.com/gdhami-net/dotnet-benchmarks.

Intel Core Ultra 9 285HX · Windows 11 · BenchmarkDotNet 0.15.8 · SDK 10.0.201 · 2026-09-01 · median of 4 quiet runs · typical spread ±3.3% · bars/tables: mean time, lower is better

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.
RESULTS
methodnet9.0net10.0ratioallocatedΔ net10.0
For_loop baseline20.8 µs ±431 ns20.2 µs ±30 ns1.00×—-3%
Foreach_loop19.6 µs ±369 ns19.2 µs ±166 ns0.95×—-2%
Linq_sum33.6 µs ±552 ns26.7 µs ±25 ns1.32×—-21%
Span_loop16.3 µs ±186 ns16.7 µs ±313 ns0.82×—+2%
RATIO VS BASELINE · net10.0
For_loop1.00×
Foreach_loop0.95×
Linq_sum1.32×
Span_loop0.82×
THE LEDGER · SAME WORKLOAD ACROSS RELEASES (LOG)
For_loopForeach_loopLinq_sumSpan_loop
29.9 µs21.1 µs14.9 µs10.0.201 · 08-22 v210.0.201 · 09-01 v3
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.
RESULTS
methodnet9.0net10.0ratioallocatedΔ net10.0
GroupBy_count baseline324.5 µs ±1.4 µs304.3 µs ±1.8 µs1.00×323.0 KB-6%
CountBy_linq130.8 µs ±764 ns102.4 µs ±783 ns0.34×47.1 KB-22%
Manual_dictionary180.0 µs ±835 ns152.3 µs ±639 ns0.50×46.9 KB-15%
RATIO VS BASELINE · net10.0
GroupBy_count1.00×
CountBy_linq0.34×
Manual_dictionary0.50×
THE LEDGER · SAME WORKLOAD ACROSS RELEASES (LOG)
GroupBy_countCountBy_linqManual_dictionary
350.8 µs178.9 µs91.2 µs10.0.201 · 08-22 v210.0.201 · 09-01 v3
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
mean time → (log)allocated → (log)For_loopForeach_loopLinq_sumSpan_loopGroupBy_countCountBy_linqManual_dictionary