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-08-22 · single run — not yet median-of-N · 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 baseline36.4 µs ±1.0 µs32.4 µs ±1.4 µs1.00×114.5 KB-11%
Naive_plus_equals2.66 ms ±513.7 µs2.77 ms ±643.8 µs85.47×23.83 MB+4%
Join_interpolation54.9 µs ±598 ns48.6 µs ±629 ns1.50×119.0 KB-11%
RATIO VS BASELINE · net10.0
StringBuilder_append
1.00×
Naive_plus_equals
85.47×
Join_interpolation
1.50×
THE LEDGER · SAME WORKLOAD ACROSS RELEASES (LOG)
StringBuilder_appendNaive_plus_equalsJoin_interpolation
3.10 ms288.7 µs26.9 µs10.0.201 · 08-22 v210.0.201 · 08-22 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 baseline187 ns ±3 ns178 ns ±2 ns1.00×-5%
SearchValues_chars187 ns ±1 ns183 ns ±2 ns1.03×-2%
Manual_loop281 ns ±4 ns266 ns ±2 ns1.50×-6%
RATIO VS BASELINE — 1000 · net10.0
IndexOfAny_chars
1.00×
SearchValues_chars
1.03×
Manual_loop
1.50×
RESULTS — 100000
methodnet9.0net10.0ratioallocatedΔ net10.0
IndexOfAny_chars baseline17.4 µs ±577 ns16.4 µs ±238 ns1.00×-5%
SearchValues_chars17.6 µs ±261 ns17.4 µs ±76 ns1.06×-1%
Manual_loop33.9 µs ±327 ns33.2 µs ±325 ns2.02×-2%
RATIO VS BASELINE — 100000 · net10.0
IndexOfAny_chars
1.00×
SearchValues_chars
1.06×
Manual_loop
2.02×
SCALING · net10.0 · MEAN TIME BY INPUT SIZE (LOG)
IndexOfAny_charsSearchValues_charsManual_loop
37.3 µs2.4 µs158 ns1000100000
THE LEDGER · SAME WORKLOAD ACROSS RELEASES (LOG)
IndexOfAny_charsSearchValues_charsManual_loop
301 ns218 ns158 ns10.0.201 · 08-22 v210.0.201 · 08-22 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