String.Create and Span<char>

Norm Bryar
4 min readOct 1, 2022

--

Photo by Adi Goldstein on Unsplash

Man, I love .Net Core. More particularly, I love the entire shift in focus from the old .Net Framework ship-vehicle-is-windows, closed-source mindset that under-invested in performance improvements, but in Core perf is at the forefront.

Here’s an interesting example in the age-old problem of string-formatting. I only recently discovered (though it’s been there since Core 2.1) another place where that perf-epiphany Span<T> makes an impact: String.Create<T>(int len, T state, SpanAction<char,T>).

Why is it cool? Everyone’s had to emit a formatted string at some point, in fact over and over and over we do. The perf-conscious among us seek to do so with a minimum of interim-string allocations and pours (e.g. avoiding long chains of logMsg += ", " + terms[i] in favor of stringBuilder.AppendJoin( “,”, terms), etc.) String.Create takes this a step further.

With String.Create, as long as you can compute the length up-front, you can supply a callback that’s given access to the final string’s tabula rasa internal char[] via a span. This allows you to write directly into any location in the string’s memory (immutability be damned! ‘cause it’s construction), with no subsequent re-alloc/pour like StringBuilder.ToString() would do. Lowest possible mem-use.

Be sure to see where you can leverage the TryFormat( Span<char>, … ) method that most object-types support, irradicating per-field, interim string allocs and their mem-copy pours, too.

public class Maven 
{
public string Name { get; set; }
public string? Motto { get; set; }
public int FollowerCount { get; set; }
public DateTime RecentActive { get; set; }

public override string ToString()
{
const string mottoLabel = "Motto: ";
const string fctLabel = "Followers: ";
const string recentLabel = "Recent: ";
const int fctLen = 5; // 9999+ or right-padded ct
const int recLen = 10; // 2022/03/25

string eol = Environment.NewLine;

int len =
Name.Length +
(Motto is not null
? eol.Length + mottoLabel.Length + Motto.Length
: 0) +
(eol.Length + fctLabel.Length + fctLen) +
(eol.Length + recentLabel.Length + recLen);

string result = string.Create(
len,
this,
(buffer, self) =>
{
Write( self.Name.AsSpan(), ref buffer);

if (Motto is not null)
{
Write( eol, ref buffer );
Write( mottoLabel, ref buffer );
Write( self.Motto, ref buffer );
}

Write( eol, ref buffer) ;
Write( fctLabel, ref buffer );
Write( self.FollowerCount > 9999
? "9999+"
: $"{self.FollowerCount,-5:g}", ref buffer );

Write( eol, ref buffer);
Write( recentLabel, ref buffer);
self.RecentActive.TryFormat(
buffer,
out int _,
"yyyy/MM/dd");
});

return result;

// ---
static void Write(
ReadOnlySpan<char> term,
ref Span<char> buf)
{
term.CopyTo( buf );
buf = buf.Slice( term.Length );
}
}
}

I like to have a utility-function, Write, to automatically advance the span. Pretty readable with that in place. You might want to add other Write overloads could handle DateTime, etc. consistently.

Ta-dah!

Above I kind’a cheated by forcing the format of numbers and dates instead of using a culturally-aware formatting, and I required a left-justified padding on the FollowerCount that ultimately takes a little more space than needed plus incurs an interpolated-string alloc. Tsk, tsk. You grasp the basics, though.

Note: if your length calculation is too big, you’ll have trailing (char)0 elements that may render poorly. Too small, the string will be silently truncated.

LINQPad output if I over-estimated length value

The TState arg will typically hold the object you want string-formatted, allowing the callback to be static, sans closures, thus fast as can be. There might be interesting cases, though, where the callback’s state param holds supporting information (culture?) as a tuple of the subject being formatted, or you’re ok with the closure cost, or …

I really should have used String.Create in the Leveraging Exception.Data story’s ToStringEx().

Anyway, enjoy!

— — — — — —

Aside: if you’re still needing to use StringBuilder in many places, you might at least want to consider putting them in a pool for re-use of their scratch buffer memory. See the Object reuse with Object Pool MSDN topic for an example.

Another aside: before this, our team wrote our own stack-based string-builder, gist of which below. It still requires a pour to get the final string, so we’ll likely be migrating to String.Create on touching those callers later.

public ref struct StackStringBuilder
{
private readonly Span<char> buffer;
public StackStringBuilder( Span<char> buf ) => buffer = buf; public int Length { get; private set; } = 0; public void Append( int x )
{
Span<char> temp = stackalloc char[ IntMaxLen ];
x.TryFormat( temp, out int xlen );
temp.Slice( 0, xlen )
.CopyTo( this.buffer.Slice( this.Length ) );
this.Length += xlen;
}
... more Append overloads ... public string ToString() =>
new string( this.buffer[ 0..this.Length ] );
}

--

--

Norm Bryar

A long-time, back-end web service developer enamored with .Net and C#, code performance, and techniques taming drudgery or increasing insight.