BENCHMARKS · ENCODING & BUFFERS

UTF-8 and Base64: allocating paths vs buffer reuse. 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

Utf8Encode

UTF-8 encode 5,000 strings: allocating GetBytes vs encoding into a rented buffer.

TAKEAWAY On hot paths that feed sockets or files, encode into a rented or pooled buffer — the bytes were always going to be copied somewhere anyway; the throwaway array in the middle is pure GC pressure.
In this run: GetBytes_rented is fastest — 3.7× faster than the baseline.
RESULTS
methodnet9.0net10.0ratioallocatedΔ net10.0
GetBytes_alloc baseline77.3 µs ±7.7 µs62.2 µs ±851 ns1.00×351.6 KB-20%
GetBytes_rented18.4 µs ±166 ns16.8 µs ±135 ns0.27×-9%
RATIO VS BASELINE · net10.0
GetBytes_alloc
1.00×
GetBytes_rented
0.27×
THE LEDGER · SAME WORKLOAD ACROSS RELEASES (LOG)

The ledger starts here — 3 run recorded. Trend lines appear from the second release onward.

THE CODE BEING MEASURED
GetBytes_alloc — what this measures

Encoding.UTF8.GetBytes — a fresh byte[] per string.

[Benchmark (Baseline)]
public int GetBytes_alloc()
{
    var total = 0;
    foreach (var s in _lines) total += Encoding.UTF8.GetBytes(s).Length;
    return total;
}
GetBytes_rented — what this measures

Encode into one rented buffer — reuse instead of allocate.

[Benchmark]
public int GetBytes_rented()
{
    var buf = ArrayPool<byte>.Shared.Rent(1024);
    var total = 0;
    foreach (var s in _lines) total += Encoding.UTF8.GetBytes(s, 0, s.Length, buf, 0);
    ArrayPool<byte>.Shared.Return(buf);
    return total;
}

Base64Encode

Base64-encode a 64 KB payload: Convert.ToBase64String vs Base64.EncodeToUtf8 into a reusable buffer.

TAKEAWAY Convert.ToBase64String makes a big UTF-16 string you usually re-encode right back to bytes. When the destination is bytes (HTTP, files), Base64.EncodeToUtf8 skips the string entirely.
In this run: EncodeToUtf8_buffer is fastest — 24.0× faster than the baseline.
RESULTS
methodnet9.0net10.0ratioallocatedΔ net10.0
ToBase64String baseline69.1 µs ±1.3 µs61.5 µs ±1.6 µs1.00×170.7 KB-11%
EncodeToUtf8_buffer2.2 µs ±333 ns2.6 µs ±165 ns0.04×+19%
RATIO VS BASELINE · net10.0
ToBase64String
1.00×
EncodeToUtf8_buffer
0.04×
THE LEDGER · SAME WORKLOAD ACROSS RELEASES (LOG)

The ledger starts here — 3 run recorded. Trend lines appear from the second release onward.

THE CODE BEING MEASURED
ToBase64String — what this measures

Convert.ToBase64String — one large string allocation.

[Benchmark (Baseline)]
public int ToBase64String()
=> Convert.ToBase64String(_payload).Length;
EncodeToUtf8_buffer — what this measures

Base64.EncodeToUtf8 into a preallocated byte buffer.

[Benchmark]
public int EncodeToUtf8_buffer()
{
    Base64.EncodeToUtf8(_payload, _dest, out _, out var written);
    return written;
}

speed vs allocation

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

GetBytes_allocGetBytes_rentedToBase64StringEncodeToUtf8_buffer
mean time → (log)allocated → (log)GetBytes_allocGetBytes_rentedToBase64StringEncodeToUtf8_buffer