Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid ToString allocations for each ISpanFormattable value in string.Concat/Join(..., IEnumerable<T>) #86521

Merged
merged 1 commit into from
May 22, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 88 additions & 109 deletions src/libraries/System.Private.CoreLib/src/System/String.Manipulation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,88 +121,8 @@ public static string Concat(params object?[] args)
return result;
}

public static string Concat<T>(IEnumerable<T> values)
{
ArgumentNullException.ThrowIfNull(values);

if (typeof(T) == typeof(char))
{
// Special-case T==char, as we can handle that case much more efficiently,
// and string.Concat(IEnumerable<char>) can be used as an efficient
// enumerable-based equivalent of new string(char[]).
using (IEnumerator<char> en = Unsafe.As<IEnumerable<char>>(values).GetEnumerator())
{
if (!en.MoveNext())
{
// There weren't any chars. Return the empty string.
return Empty;
}

char c = en.Current; // save the first char

if (!en.MoveNext())
{
// There was only one char. Return a string from it directly.
return CreateFromChar(c);
}

// Create the StringBuilder, add the chars we've already enumerated,
// add the rest, and then get the resulting string.
var result = new ValueStringBuilder(stackalloc char[StackallocCharBufferSizeLimit]);
result.Append(c); // first value
do
{
c = en.Current;
result.Append(c);
}
while (en.MoveNext());
return result.ToString();
}
}
else
{
using (IEnumerator<T> en = values.GetEnumerator())
{
if (!en.MoveNext())
return string.Empty;

// We called MoveNext once, so this will be the first item
T currentValue = en.Current;

// Call ToString before calling MoveNext again, since
// we want to stay consistent with the below loop
// Everything should be called in the order
// MoveNext-Current-ToString, unless further optimizations
// can be made, to avoid breaking changes
string? firstString = currentValue?.ToString();

// If there's only 1 item, simply call ToString on that
if (!en.MoveNext())
{
// We have to handle the case of either currentValue
// or its ToString being null
return firstString ?? string.Empty;
}

var result = new ValueStringBuilder(stackalloc char[StackallocCharBufferSizeLimit]);

result.Append(firstString);

do
{
currentValue = en.Current;

if (currentValue != null)
{
result.Append(currentValue.ToString());
}
}
while (en.MoveNext());

return result.ToString();
}
}
}
public static string Concat<T>(IEnumerable<T> values) =>
JoinCore(ReadOnlySpan<char>.Empty, values);

public static string Concat(IEnumerable<string?> values)
{
Expand Down Expand Up @@ -891,48 +811,102 @@ private static string JoinCore<T>(ReadOnlySpan<char> separator, IEnumerable<T> v
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.values);
}

using (IEnumerator<T> en = values.GetEnumerator())
using (IEnumerator<T> e = values.GetEnumerator())
{
if (!en.MoveNext())
if (!e.MoveNext())
{
// If the enumerator is empty, just return an empty string.
return Empty;
}

// We called MoveNext once, so this will be the first item
T currentValue = en.Current;

// Call ToString before calling MoveNext again, since
// we want to stay consistent with the below loop
// Everything should be called in the order
// MoveNext-Current-ToString, unless further optimizations
// can be made, to avoid breaking changes
string? firstString = currentValue?.ToString();

// If there's only 1 item, simply call ToString on that
if (!en.MoveNext())
if (typeof(T) == typeof(char))
{
// We have to handle the case of either currentValue
// or its ToString being null
return firstString ?? Empty;
}
// Special-case T==char, as we can handle that case much more efficiently,
// and string.Concat(IEnumerable<char>) can be used as an efficient
// enumerable-based equivalent of new string(char[]).

var result = new ValueStringBuilder(stackalloc char[StackallocCharBufferSizeLimit]);
IEnumerator<char> en = Unsafe.As<IEnumerator<char>>(e);

result.Append(firstString);
char c = en.Current; // save the first value
if (!en.MoveNext())
{
// There was only one char. Return a string from it directly.
return CreateFromChar(c);
}

do
// Create the builder, add the char we already enumerated,
// add the rest, and then get the resulting string.
var result = new ValueStringBuilder(stackalloc char[StackallocCharBufferSizeLimit]);
result.Append(c); // first value
do
{
if (!separator.IsEmpty)
{
result.Append(separator);
}

c = en.Current;
result.Append(c);
}
while (en.MoveNext());
return result.ToString();
}
else if (typeof(T).IsValueType && default(T) is ISpanFormattable)
{
currentValue = en.Current;
// Special-case value types that are ISpanFormattable, as we can implement those to avoid
// all string allocations for the individual values. We only do this for value types because
// a) value types are much more likely to implement ISpanFormattable, and b) we can use
// DefaultInterpolatedStringHandler to do all the heavy lifting, and it's more efficient
// for value types as the checks it does for interface implementations are all elided.

T value = e.Current; // save the first value
if (!e.MoveNext())
{
// There was only one value. Return a string from it directly.
return value!.ToString() ?? Empty;
}

result.Append(separator);
if (currentValue != null)
var result = new DefaultInterpolatedStringHandler(0, 0, CultureInfo.CurrentCulture, stackalloc char[StackallocCharBufferSizeLimit]);
result.AppendFormatted(value); // first value
do
{
result.Append(currentValue.ToString());
if (!separator.IsEmpty)
{
result.AppendFormatted(separator);
}

result.AppendFormatted(e.Current);
}
while (e.MoveNext());
return result.ToStringAndClear();
}
while (en.MoveNext());
else
{
// For all other Ts, fall back to calling ToString on each and appending the resulting
// string to a builder.

return result.ToString();
string? firstString = e.Current?.ToString(); // save the first value
if (!e.MoveNext())
{
return firstString ?? Empty;
}

var result = new ValueStringBuilder(stackalloc char[StackallocCharBufferSizeLimit]);

result.Append(firstString);
do
{
if (!separator.IsEmpty)
{
result.Append(separator);
}

result.Append(e.Current?.ToString());
}
while (e.MoveNext());

return result.ToString();
}
}
}

Expand Down Expand Up @@ -965,6 +939,11 @@ private static string JoinCore(ReadOnlySpan<char> separator, ReadOnlySpan<string
}
}

if (totalLength == 0)
{
return Empty;
}

// Copy each of the strings into the result buffer, interleaving with the separator.
string result = FastAllocateString(totalLength);
int copiedLength = 0;
Expand Down