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
- Allow safe usage of stack memory
- Use pooled memory when needed to reduce GC pressure
- Allow dynamic and explicit capacity growth
- Facilitate interop scenarios (i.e. passing as
char* szValue) - Follow API semantics of StringBuilder & string where possible
- 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
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
ValueStringBuilderfor this purpose internally. It starts with an optional initial buffer (which we often stackalloc) and will grow using ArrayPool if needed.Design Goals
char* szValue)API
Here is the proposed API:
This is the current shape of our internal
ValueStringBuilder: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
bool TryGet*(Span)overload? (see https://github.com/dotnet/coreclr/pull/17097/files#r176560435)Dispose()in ausingstatement? (Proposal: Allow Dispose by Convention csharplang#93)AppendFormatoverloads?AppendSpan()is a little tricky to grok- is there a better term/pattern?