-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Reduce allocations in StringBuilder.AppendFormat for primitive types #15110
Conversation
Use the new `TryFormat` APIs to avoid string allocations for primitive types inside `StringBuilder.AppendFormat`, used by `string.Format`/interpolated strings.
| [StructLayout(LayoutKind.Sequential)] | ||
| [TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")] | ||
| public struct Byte : IComparable, IConvertible, IFormattable, IComparable<Byte>, IEquatable<Byte> | ||
| public struct Byte : IComparable, IConvertible, IFormattable, IComparable<Byte>, IEquatable<Byte>, ISpanFormattable |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jkotas, are there any concerns with implementing additional internal interfaces on these primitive types? Mostly wondering about AOT and whether this significantly impacts size and the like.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is fine on non-generic types. It is a fixed cost for them.
The common generic types is where this hurts - it gets multiplied for them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is fine on non-generic types.
Cool, thanks.
| IFormattable formattableArg = arg as IFormattable; | ||
| // If arg is ISpanFormattable and the beginning doesn't need padding, | ||
| // try formatting it into the remaining current chunk. | ||
| if (arg is ISpanFormattable spanFormattableArg && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this have any measurable impact on the common case where arg isn't ISpanFormattable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It appears to be within noise between benchmark runs. Sometimes it's slightly slower, sometimes faster.
Benchmark:
using System;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Attributes.Jobs;
using BenchmarkDotNet.Running;
[MemoryDiagnoser]
[InProcess]
public class Program
{
public static void Main() => BenchmarkRunner.Run<Program>();
const string Format = "{0} blah";
private static readonly Foo s_foo = new Foo();
private static readonly object s_boxedValueFoo = new ValueFoo();
private static readonly Bar s_bar = new Bar();
private static readonly object s_boxedValueBar = new ValueBar();
[Benchmark]
public string ClassIFormattable() => string.Format(Format, s_foo);
[Benchmark]
public string StructIFormattable() => string.Format(Format, s_boxedValueFoo);
[Benchmark]
public string ClassToString() => string.Format(Format, s_bar);
[Benchmark]
public string StructToString() => string.Format(Format, s_boxedValueBar);
class Foo : IFormattable
{
public string ToString(string format, IFormatProvider formatProvider) => "Foo";
}
struct ValueFoo : IFormattable
{
public string ToString(string format, IFormatProvider formatProvider) => "Foo";
}
class Bar
{
public override string ToString() => "Bar";
}
struct ValueBar
{
public override string ToString() => "Bar";
}
}Before (3 runs):
Method | Mean | Error | StdDev | Gen 0 | Allocated |
------------------- |----------:|----------:|----------:|-------:|----------:|
ClassIFormattable | 81.70 ns | 0.3733 ns | 0.3309 ns | 0.0113 | 48 B |
StructIFormattable | 81.87 ns | 0.4593 ns | 0.4296 ns | 0.0113 | 48 B |
ClassToString | 80.22 ns | 0.4537 ns | 0.4244 ns | 0.0113 | 48 B |
StructToString | 81.96 ns | 0.3142 ns | 0.2939 ns | 0.0113 | 48 B |
Method | Mean | Error | StdDev | Gen 0 | Allocated |
------------------- |----------:|----------:|----------:|-------:|----------:|
ClassIFormattable | 78.52 ns | 0.3004 ns | 0.2663 ns | 0.0113 | 48 B |
StructIFormattable | 81.08 ns | 0.4321 ns | 0.3830 ns | 0.0113 | 48 B |
ClassToString | 77.51 ns | 0.4508 ns | 0.4217 ns | 0.0113 | 48 B |
StructToString | 79.25 ns | 0.5172 ns | 0.4838 ns | 0.0113 | 48 B |
Method | Mean | Error | StdDev | Gen 0 | Allocated |
------------------- |----------:|----------:|----------:|-------:|----------:|
ClassIFormattable | 79.28 ns | 0.4490 ns | 0.3980 ns | 0.0113 | 48 B |
StructIFormattable | 80.23 ns | 0.3812 ns | 0.3379 ns | 0.0113 | 48 B |
ClassToString | 76.87 ns | 0.5829 ns | 0.5452 ns | 0.0113 | 48 B |
StructToString | 78.07 ns | 0.6419 ns | 0.6005 ns | 0.0113 | 48 B |
After (3 runs):
Method | Mean | Error | StdDev | Gen 0 | Allocated |
------------------- |----------:|----------:|----------:|-------:|----------:|
ClassIFormattable | 79.82 ns | 0.9359 ns | 0.8755 ns | 0.0113 | 48 B |
StructIFormattable | 81.11 ns | 0.2301 ns | 0.2040 ns | 0.0113 | 48 B |
ClassToString | 80.21 ns | 0.2605 ns | 0.2310 ns | 0.0113 | 48 B |
StructToString | 81.55 ns | 0.2644 ns | 0.2473 ns | 0.0113 | 48 B |
Method | Mean | Error | StdDev | Gen 0 | Allocated |
------------------- |----------:|----------:|----------:|-------:|----------:|
ClassIFormattable | 78.34 ns | 0.5466 ns | 0.5113 ns | 0.0113 | 48 B |
StructIFormattable | 78.37 ns | 0.4490 ns | 0.4200 ns | 0.0113 | 48 B |
ClassToString | 78.64 ns | 0.3787 ns | 0.3542 ns | 0.0113 | 48 B |
StructToString | 80.15 ns | 0.8122 ns | 0.7597 ns | 0.0113 | 48 B |
Method | Mean | Error | StdDev | Gen 0 | Allocated |
------------------- |----------:|----------:|----------:|-------:|----------:|
ClassIFormattable | 79.84 ns | 0.3730 ns | 0.3489 ns | 0.0113 | 48 B |
StructIFormattable | 79.59 ns | 0.3887 ns | 0.3636 ns | 0.0113 | 48 B |
ClassToString | 80.77 ns | 0.3372 ns | 0.3154 ns | 0.0113 | 48 B |
StructToString | 81.25 ns | 0.2619 ns | 0.2450 ns | 0.0113 | 48 B |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok. Thanks for following up.
|
Nice improvement. |
|
@dotnet-bot test CentOS7.1 x64 Checked Innerloop Build and Test please |
|
@dotnet-bot test CentOS7.1 x64 Checked Innerloop Build and Test please ("Could not load type 'System.MemoryExtensions'") |
|
@jkotas, it's ok to merge with these known type load exceptions on centos, right? |
Use the new
TryFormatAPIs to avoid string allocations for primitive types insideStringBuilder.AppendFormat, used bystring.Format/interpolated strings.Benchmark:
Before:
After:
Additional improvements can be looked into subsequently, such as avoiding any
itemFormatstring allocations.