Skip to content

feat: refactor AppendFormattedValue in GeneratedQueryStringBuilder#2246

Closed
TimothyMakkison wants to merge 1 commit into
reactiveui:mainfrom
TimothyMakkison:refactor_append_format
Closed

feat: refactor AppendFormattedValue in GeneratedQueryStringBuilder#2246
TimothyMakkison wants to merge 1 commit into
reactiveui:mainfrom
TimothyMakkison:refactor_append_format

Conversation

@TimothyMakkison

@TimothyMakkison TimothyMakkison commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Refactor AppendFormattedValue to use ValiueStringBuilder and inline FormatWithGrowth

@TimothyMakkison
TimothyMakkison force-pushed the refactor_append_format branch from 013dfe8 to 2ffc0b5 Compare July 15, 2026 13:52
@glennawatson

glennawatson commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

@TimothyMakkison benchmarked this on .NET 10 - the ValueStringBuilder scratch idea is good, but the current form regresses the hot path and leaks the pool buffer. Suggesting a small "hybrid" tweak that keeps the clarity and fixes both.

Hybrid AppendFormattedValue body

using var buffer = new ValueStringBuilder(stackalloc char[FormatBufferLength]);
int written;
while (!value.TryFormat(buffer.RawChars, out written, format.AsSpan(), CultureInfo.InvariantCulture))
{
    buffer.EnsureCapacity(buffer.Capacity * BufferGrowthFactor);
}

var formatted = buffer.RawChars[..written];
if (escape)
{
    StringHelpers.AppendUriDataEscaped(ref target, formatted);
    return;
}

formatted.CopyTo(target.AppendSpan(written));

Two changes vs this PR:

  • using - the PR never disposes the scratch, so a value longer than 128 chars leaks the rented array. using returns it.
  • RawChars[..written] + CopyTo(target.AppendSpan(written)) instead of buffer.Length = written; target.Append(buffer.AsSpan()) - the AsSpan/Append path is what regressed the verbatim case.

Numbers (net10, Ryzen 7 5800X, in-process; main = 1.00, >1 slower)

value / mode this PR hybrid
int / verbatim 1.24 1.09
int / escaped 1.18 1.21
long / verbatim 1.10 1.02
double / verbatim 1.06 1.02
Guid / verbatim 0.91 0.87

Allocations: zero in all three - identical, no change either way.

@glennawatson

Copy link
Copy Markdown
Contributor

The version in main still performs the best though.

@glennawatson

Copy link
Copy Markdown
Contributor

@TimothyMakkison you okay if we go for the hybrid approach documented above for this one. Yes it's a little less performant then the version in main, but, I think it combines the easier maintainability you introduced in this PR

@glennawatson

Copy link
Copy Markdown
Contributor

Thanks @TimothyMakkison! I've folded your feedback into AppendFormattedValue on main as part of the recent performance work (#2263) - the scratch buffer, the extracted FormatWithGrowth, the leak-free buffer return, and the CopyTo(AppendSpan) fast path all landed there, and it benchmarks as the fastest of the options. So I'll close this one out, but it genuinely shaped where things ended up - much appreciated.

@TimothyMakkison

TimothyMakkison commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator Author

Sorry, never got round to replying, happy you merged.

Had been meaning to write my own benchmarks like yours to compare the approaches, as I was surprised by the differences and wanted to know how many ns each operation took. I assume all of the slow down is caused by the instantiation of the ValueStringBuilder struct

I do wonder if it is worth decreasing the size of the initial stackalloc buffer as zeroing the stack does have a small cost (and most of the likely ISpanFormattable types are usually less that 32 characters).

I've been meaning to mention this along with the prospect of using [SkipLocalsInit] I do have a notepad with other ideas but haven't got round to proof reading it

image

https://www.meziantou.net/csharp-9-improve-performance-using-skiplocalsinit.htm

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants