BENCHMARKS · STRINGS & SEARCH

Building text and finding characters — the everyday hot paths. 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

BuildReport

Build a 1,000-line CSV report four ways — including the naive += loop everyone writes first.

TAKEAWAY Never build strings with += in a loop - it recopies everything each pass. StringBuilder for incremental building; Join/interpolation is fine for one-shot projections.
In this run: StringBuilder_append is fastest (it is the baseline).
RESULTS
methodnet9.0net10.0ratioallocatedΔ net10.0
StringBuilder_append baseline35.8 µs ±458 ns30.3 µs ±340 ns1.00×114.5 KB-15%
Naive_plus_equals2.17 ms ±52.3 µs2.47 ms ±55.8 µs81.52×23.83 MB+13%
Join_interpolation54.2 µs ±400 ns46.4 µs ±469 ns1.53×119.0 KB-14%
RATIO VS BASELINE · net10.0
StringBuilder_append1.00×
Naive_plus_equals81.52×
Join_interpolation1.53×
THE LEDGER · SAME WORKLOAD ACROSS RELEASES (LOG)
StringBuilder_appendNaive_plus_equalsJoin_interpolation
3.02 ms284.7 µs26.9 µs10.0.201 · 08-22 v210.0.201 · 09-01 v3
THE CODE BEING MEASURED
StringBuilder_append — what this measures

StringBuilder with chained Appends — the textbook answer.

[Benchmark (Baseline)]
public string StringBuilder_append()
{
    var sb = new StringBuilder();
    foreach (var o in _orders)
        sb.Append(o.Id).Append(';').Append(o.Customer).Append(';').Append(o.Total).AppendLine();
    return sb.ToString();
}
Naive_plus_equals — what this measures

Naive string += in a loop — quadratic: every iteration copies everything so far.

[Benchmark]
public string Naive_plus_equals()
{
    var s = "";
    foreach (var o in _orders)
        s += o.Id + ";" + o.Customer + ";" + o.Total + "\n";
    return s;
}
Join_interpolation — what this measures

string.Join over an interpolated LINQ projection.

[Benchmark]
public string Join_interpolation() =>
    string.Join('\n', _orders.Select(o => $"
{o.Id}

FindDelimiters

Find delimiter characters in a haystack: classic IndexOfAny vs .NET 8+ SearchValues vs a manual loop.

TAKEAWAY IndexOfAny with a small char set is already vectorized in modern .NET - SearchValues matches it here and pulls ahead as the set grows or matches get rarer. The hand-written loop is the only clear loser; either vectorized API halves it.
In this run: IndexOfAny_chars is fastest (it is the baseline).
RESULTS — 1000
methodnet9.0net10.0ratioallocatedΔ net10.0
IndexOfAny_chars baseline181 ns ±0 ns178 ns ±0 ns1.00×—-2%
SearchValues_chars185 ns ±0 ns190 ns ±0 ns1.06×—+3%
Manual_loop281 ns ±0 ns267 ns ±3 ns1.50×—-5%
RATIO VS BASELINE — 1000 · net10.0
IndexOfAny_chars1.00×
SearchValues_chars1.06×
Manual_loop1.50×
RESULTS — 100000
methodnet9.0net10.0ratioallocatedΔ net10.0
IndexOfAny_chars baseline16.8 µs ±24 ns16.7 µs ±38 ns1.00×—-1%
SearchValues_chars17.8 µs ±15 ns17.6 µs ±12 ns1.06×—-1%
Manual_loop34.1 µs ±167 ns33.5 µs ±247 ns2.01×—-2%
RATIO VS BASELINE — 100000 · net10.0
IndexOfAny_chars1.00×
SearchValues_chars1.06×
Manual_loop2.01×
SCALING · net10.0 · MEAN TIME BY INPUT SIZE (LOG)
IndexOfAny_charsSearchValues_charsManual_loop
37.6 µs2.4 µs159 ns1000100000
THE LEDGER · SAME WORKLOAD ACROSS RELEASES (LOG)
IndexOfAny_charsSearchValues_charsManual_loop
301 ns219 ns159 ns10.0.201 · 08-22 v210.0.201 · 09-01 v3
THE CODE BEING MEASURED
IndexOfAny_chars — what this measures

string.IndexOfAny with a char array — the pre-.NET 8 idiom.

[Benchmark (Baseline)]
public int IndexOfAny_chars()
{
    var count = 0;
    var span = _haystack.AsSpan();
    int i;
    while ((i = span.IndexOfAny(DelimiterChars)) >= 0)
    {
        count++;
        span = span[(i + 1)..];
    }
    return count;
}
SearchValues_chars — what this measures

SearchValues (.NET 8+): precomputed, vectorized set search.

[Benchmark]
public int SearchValues_chars()
{
    var count = 0;
    var span = _haystack.AsSpan();
    int i;
    while ((i = span.IndexOfAny(DelimiterSearch)) >= 0)
    {
        count++;
        span = span[(i + 1)..];
    }
    return count;
}
Manual_loop — what this measures

A hand-written character loop — what the "I'll just write it myself" instinct produces.

[Benchmark]
public int Manual_loop()
{
    var count = 0;
    foreach (var c in _haystack)
        if (c is ';' or ',' or '\t' or '|') count++;
    return count;
}

speed vs allocation

Fast is one axis. What it costs the GC is the other.

StringBuilder_appendNaive_plus_equalsJoin_interpolationIndexOfAny_charsSearchValues_charsManual_loop
mean time → (log)allocated → (log)StringBuilder_appendNaive_plus_equalsJoin_interpolationIndexOfAny_charsSearchValues_charsManual_loop