Skip to content

API Proposal: Add a ValueStringBuilder #25587

Description

@JeremyKuhne

We should consider making a value based StringBuilder to allow low allocation building of strings. While Span allows you to provide a writable buffer, in many scenarios we have a need to get or build strings and we don't know precisely how much space will be needed ahead of time. Having an abstraction that can grow beyond a given initial buffer is particularly useful as it doesn't require looping with Try* APIs- which can be both complicated and have negative performance implications.

We currently use ValueStringBuilder for this purpose internally. It starts with an optional initial buffer (which we often stackalloc) and will grow using ArrayPool if needed.

Design Goals

  1. Allow safe usage of stack memory
  2. Use pooled memory when needed to reduce GC pressure
  3. Allow dynamic and explicit capacity growth
  4. Facilitate interop scenarios (i.e. passing as char* szValue)
  5. Follow API semantics of StringBuilder & string where possible
  6. Be stack allocated

API

Here is the proposed API:

namespaceSystem.Text{publicrefstructValueStringBuilder{publicValueStringBuilder(Span<char>initialBuffer);// The logical length of the builder (end of the "string")publicintLength{get;set;}// Available space in charspublicintCapacity{get;}// Ensure there is at least this amount of spacepublicvoidEnsureCapacity(intcapacity);// Get a pinnable reference to the builder. "terminate" ensures the builder has a null char after Length in the buffer.publicrefcharGetPinnableReference(boolterminate=false);// Indexer, allows setting/getting individual charspublicrefcharthis[intindex]{get;}// Returns a string based off of the current positionpublicoverridestringToString();// Returns a span around the contents of the builder. "terminate" ensures the builder has a null char after Length in the buffer.publicReadOnlySpan<char>AsSpan(boolterminate);// To ensure inlining perf, we have a separate overload for terminatepublicReadOnlySpan<char>AsSpan();publicboolTryCopyTo(Span<char>destination,outintcharsWritten);publicvoidInsert(intindex,charvalue,intcount=1);publicvoidInsert(intindex,ReadOnlySpan<char>value,intcount=1);publicvoidAppend(charc,intcount=1);publicvoidAppend(ReadOnlySpan<char>value);// This gives you an appended span that you can write topublicSpan<char>AppendSpan(intlength);// Returns any ArrayPool buffer that may have been rentedpublicvoidDispose()}}

This is the current shape of our internal ValueStringBuilder:

namespaceSystem.Text{internalrefstructValueStringBuilder{publicValueStringBuilder(Span<char>initialBuffer);publicintLength{get;set;}publicintCapacity{get;}publicvoidEnsureCapacity(intcapacity);/// <summary>/// Get a pinnable reference to the builder./// </summary>/// <param name="terminate">Ensures that the builder has a null char after <see cref="Length"/></param>publicrefcharGetPinnableReference(boolterminate=false);publicrefcharthis[intindex]{get;}// Returns a string based off of the current positionpublicoverridestringToString();/// <summary>/// Returns a span around the contents of the builder./// </summary>/// <param name="terminate">Ensures that the builder has a null char after <see cref="Length"/></param>publicReadOnlySpan<char>AsSpan(boolterminate);// To ensure inlining perf, we have a separate overload for terminatepublicReadOnlySpan<char>AsSpan();publicboolTryCopyTo(Span<char>destination,outintcharsWritten);publicvoidInsert(intindex,charvalue,intcount);publicvoidAppend(charc);publicvoidAppend(strings);publicvoidAppend(charc,intcount);publicunsafevoidAppend(char*value,intlength);publicvoidAppend(ReadOnlySpan<char>value);// This gives you an appended span that you can write topublicSpan<char>AppendSpan(intlength);// Returns any ArrayPool buffer that may have been rentedpublicvoidDispose()}}

Sample Code

Here is a common pattern on an API that could theoretically be made public if ValueStringBuilder was public:
(Although we would call this one GetFullUserName or something like that.)

https://github.com/dotnet/corefx/blob/050bc33738887d9d8fcc9bc5965b7d9ca65bc7f4/src/System.Runtime.Extensions/src/System/Environment.Win32.cs#L40-L56

The caller is above this method:

https://github.com/dotnet/corefx/blob/050bc33738887d9d8fcc9bc5965b7d9ca65bc7f4/src/System.Runtime.Extensions/src/System/Environment.Win32.cs#L13-L38

Usage of AppendSpan:

https://github.com/dotnet/corefx/blob/3538128fa1fb2b77a81026934d61cd370a0fd7f5/src/System.Runtime.Numerics/src/System/Numerics/BigNumber.cs#L550-L560

I'll add more usage details and possible API surface area.

Notes

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    api-needs-workAPI needs work before it is approved, it is NOT ready for implementationarea-System.RuntimeblockedIssue/PR is blocked on something - see comments

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions