Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Conversation

@justinvp
Copy link

Use the new TryFormat APIs to avoid string allocations for primitive types inside StringBuilder.AppendFormat, used by string.Format/interpolated strings.

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>();

    public static readonly Guid s_guid = new Guid("8759798E-3430-47AA-9A53-C6813F1A7456");

    [Benchmark]
    public void StringFormat()
    {
        Guid guid = s_guid;
        int value = 42;
        for (int i = 0; i < 100_000; i++) string.Format("{0} blah {1}", guid, value);
    }
}

Before:

       Method |     Mean |     Error |    StdDev |     Gen 0 | Allocated |
------------- |---------:|----------:|----------:|----------:|----------:|
 StringFormat | 20.78 ms | 0.1334 ms | 0.1183 ms | 7437.5000 |  29.75 MB |

After:

       Method |     Mean |     Error |    StdDev |     Gen 0 | Allocated |
------------- |---------:|----------:|----------:|----------:|----------:|
 StringFormat | 19.80 ms | 0.0735 ms | 0.0688 ms | 4187.5000 |  16.78 MB |

Additional improvements can be looked into subsequently, such as avoiding any itemFormat string allocations.

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
Copy link
Member

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.

Copy link
Member

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.

Copy link
Member

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 &&
Copy link
Member

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?

Copy link
Author

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 |

Copy link
Member

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.

@stephentoub
Copy link
Member

Nice improvement.

@stephentoub
Copy link
Member

@dotnet-bot test CentOS7.1 x64 Checked Innerloop Build and Test please

@stephentoub
Copy link
Member

@dotnet-bot test CentOS7.1 x64 Checked Innerloop Build and Test please ("Could not load type 'System.MemoryExtensions'")

@stephentoub
Copy link
Member

@jkotas, it's ok to merge with these known type load exceptions on centos, right?

@jkotas
Copy link
Member

jkotas commented Nov 21, 2017

@jkotas jkotas merged commit b3005fe into dotnet:master Nov 21, 2017
@justinvp justinvp deleted the appendformat branch November 21, 2017 17:10
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants