Building text and finding characters — the everyday hot paths. Suite and raw reports: github.com/gdhami-net/dotnet-benchmarks.
BuildReport
Build a 1,000-line CSV report four ways — including the naive += loop everyone writes first.
In this run: StringBuilder_append is fastest (it is the baseline).
| method | net9.0 | net10.0 | ratio | allocated | Δ net10.0 |
|---|---|---|---|---|---|
| StringBuilder_append baseline | 36.4 µs ±1.0 µs | 32.4 µs ±1.4 µs | 1.00× | 114.5 KB | -11% |
| Naive_plus_equals | 2.66 ms ±513.7 µs | 2.77 ms ±643.8 µs | 85.47× | 23.83 MB | +4% |
| Join_interpolation | 54.9 µs ±598 ns | 48.6 µs ±629 ns | 1.50× | 119.0 KB | -11% |
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.
In this run: IndexOfAny_chars is fastest (it is the baseline).
| method | net9.0 | net10.0 | ratio | allocated | Δ net10.0 |
|---|---|---|---|---|---|
| IndexOfAny_chars baseline | 187 ns ±3 ns | 178 ns ±2 ns | 1.00× | — | -5% |
| SearchValues_chars | 187 ns ±1 ns | 183 ns ±2 ns | 1.03× | — | -2% |
| Manual_loop | 281 ns ±4 ns | 266 ns ±2 ns | 1.50× | — | -6% |
| method | net9.0 | net10.0 | ratio | allocated | Δ net10.0 |
|---|---|---|---|---|---|
| IndexOfAny_chars baseline | 17.4 µs ±577 ns | 16.4 µs ±238 ns | 1.00× | — | -5% |
| SearchValues_chars | 17.6 µs ±261 ns | 17.4 µs ±76 ns | 1.06× | — | -1% |
| Manual_loop | 33.9 µs ±327 ns | 33.2 µs ±325 ns | 2.02× | — | -2% |
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.