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-08-22 · single run — not yet median-of-N · 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.1 µs ±660 ns20.2 µs ±36 ns1.00×+1%
Foreach_loop18.9 µs ±107 ns19.3 µs ±318 ns0.96×+3%
Linq_sum32.7 µs ±132 ns27.4 µs ±36 ns1.35×-16%
Span_loop16.1 µs ±13 ns16.4 µs ±357 ns0.81×+2%
RATIO VS BASELINE · net10.0
For_loop
1.00×
Foreach_loop
0.96×
Linq_sum
1.35×
Span_loop
0.81×
THE LEDGER · SAME WORKLOAD ACROSS RELEASES (LOG)
For_loopForeach_loopLinq_sumSpan_loop
30.7 µs21.2 µs14.6 µs10.0.201 · 08-22 v210.0.201 · 08-22 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.2× faster than the baseline.
RESULTS
methodnet9.0net10.0ratioallocatedΔ net10.0
GroupBy_count baseline352.8 µs ±30.1 µs343.2 µs ±24.2 µs1.00×323.0 KB-3%
CountBy_linq130.3 µs ±838 ns106.0 µs ±1.9 µs0.31×47.1 KB-19%
Manual_dictionary179.4 µs ±1.5 µs154.5 µs ±3.3 µs0.45×46.9 KB-14%
RATIO VS BASELINE · net10.0
GroupBy_count
1.00×
CountBy_linq
0.31×
Manual_dictionary
0.45×
THE LEDGER · SAME WORKLOAD ACROSS RELEASES (LOG)
GroupBy_countCountBy_linqManual_dictionary
385.1 µs189.2 µs93.0 µs10.0.201 · 08-22 v210.0.201 · 08-22 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