-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Closed as not planned
Closed as not planned
Copy link
Labels
area-System.Runtimein-prThere is an active PR which will close this issue when it is mergedThere is an active PR which will close this issue when it is mergedneeds-further-triageIssue has been initially triaged, but needs deeper consideration or reconsiderationIssue has been initially triaged, but needs deeper consideration or reconsideration
Description
Currently, StringBuilder.Append(Object) is calling Object.ToString to get a string representation of the object passed as a parameter:
| public StringBuilder Append(object? value) => (value == null) ? this : Append(value.ToString()); |
Can we consider adding support for objects that implement ISpanFormattable here instead of calling ToString for them? This will allow to write directly to the underlying buffer rather than allocate a temporary string.
Currently to avoid the temporary string when appending an object, we can use the overload Append(ref AppendInterpolatedStringHandler) but it's not intuitive to use an interpolated string just to append an object like:
Version version = new() { Major = 2, Minor = 3, Patch = 140 };
var sb = new StringBuilder();
sb.Append(version); // ToString will be called here and a temporary string will be allocated
sb.Append($"{version}"); // ISpanFormattable.TryFormat will be called here
var result = sb.ToString();
public struct Version : ISpanFormattable
{
const int Int32NumberBufferLength = 10 + 1; // 10 for the longest input: 2,147,483,647. We need 1 additional byte for the terminating null
public int Major { get; init; }
public int Minor { get; init; }
public int Patch { get; init; }
public override string ToString()
{
Span<char> destination = stackalloc char[(3 * Int32NumberBufferLength) + 3]; // at most 3 Int32s and 3 periods
_ = TryFormatCore(destination, out int charsWritten);
return destination.Slice(0, charsWritten).ToString();
}
public string ToString(string? format, IFormatProvider? formatProvider)
{
return ToString();
}
public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider? provider)
{
return TryFormatCore(destination, out charsWritten);
}
private bool TryFormatCore(Span<char> destination, out int charsWritten)
{
return destination.TryWrite($"{Major}.{Minor}.{Patch}", out charsWritten);
}
}Benchmark:
[MemoryDiagnoser]
[HideColumns("Error", "StdDev", "Median", "RatioSD")]
public class Bench
{
private readonly Version _version = new() { Major = 2, Minor = 3, Patch = 140 };
[Benchmark]
public string AppendObject()
{
var sb = new StringBuilder();
sb.Append(_version);
return sb.ToString();
}
[Benchmark]
public string AppendInterpolated()
{
var sb = new StringBuilder();
sb.Append($"{_version}");
return sb.ToString();
}
}| Method | Mean | Gen0 | Allocated |
|---|---|---|---|
| AppendObject | 78.03 ns | 0.1032 | 216 B |
| AppendInterpolated | 45.64 ns | 0.0688 | 144 B |
Metadata
Metadata
Assignees
Labels
area-System.Runtimein-prThere is an active PR which will close this issue when it is mergedThere is an active PR which will close this issue when it is mergedneeds-further-triageIssue has been initially triaged, but needs deeper consideration or reconsiderationIssue has been initially triaged, but needs deeper consideration or reconsideration