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

Optimize using StringBuilder #24755

Merged
3 commits merged into from
Apr 27, 2021
Merged

Optimize using StringBuilder #24755

3 commits merged into from
Apr 27, 2021

Conversation

SergerGood
Copy link
Contributor

No description provided.

@ArminShoeibi
Copy link

Nice.
https://stackoverflow.com/questions/42721009/performance-of-stringbuilder-append

@roji
Copy link
Member

roji commented Apr 25, 2021

@SergerGood any specific reason to do this change? Are you seeing performance differences between appending a char and a string?

@ArminShoeibi your link seems to point to a Java-related question, not .NET.

@@ -44,12 +44,12 @@ string ToDebugString(MetadataDebugStringOptions options = MetadataDebugStringOpt
var singleLine = (options & MetadataDebugStringOptions.SingleLine) != 0;
if (singleLine)
{
builder.Append($"ViewColumn: {Table.Name}.");
builder.Append("ViewColumn: ").Append(Table.Name).Append('.');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes the code less readable, in a codepath that is not perf-sensitive in any way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the same place:

builder.Append(Name).Append(" (");

builder.Append(StoreType).Append(")");

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still doesn't seem like an improvement to me (note that here there are three things, whereas in your two examples there are two). Can you please revert unless there's a good reason to do this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if it's good reason or not.

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042
Intel Core i5-9300H CPU 2.40GHz, 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=6.0.100-preview.3.21202.5
  [Host]     : .NET Core 6.0.0 (CoreCLR 6.0.21.20104, CoreFX 6.0.21.20104), X64 RyuJIT
  DefaultJob : .NET Core 6.0.0 (CoreCLR 6.0.21.20104, CoreFX 6.0.21.20104), X64 RyuJIT
Method CharactersCount Mean Error StdDev Median Gen 0 Gen 1 Gen 2 Allocated
StringInterpolation 5 32.56 ns 0.679 ns 1.764 ns 32.28 ns 0.0134 - - 56 B
Append 5 14.08 ns 0.309 ns 0.356 ns 14.13 ns - - - -
StringInterpolation 10 35.03 ns 0.714 ns 1.046 ns 35.00 ns 0.0153 - - 64 B
Append 10 14.56 ns 0.903 ns 2.502 ns 13.31 ns - - - -
Benchmark code
  [MemoryDiagnoser]
  public class Program
  {
      private StringBuilder _builder;
      private string _text;
  
      [Params(5, 10)]
      public int CharactersCount { get; set; }
  
      [Benchmark]
      public void StringInterpolation()
      {
          _builder.Clear();
          _builder.Append($"Column: {_text}.");
      }
  
      [Benchmark]
      public void Append()
      {
          _builder.Clear();
          _builder.Append("Column: ").Append(_text).Append('.');
      }
  
      [GlobalSetup]
      public void Setup()
      {
          _builder = new StringBuilder();
          _text = new string('a', CharactersCount);
      }
  
      private static void Main() => BenchmarkRunner.Run<Program>();
  }

Copy link
Member

@roji roji Apr 26, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String interpolation indeed has a perf cost, but this code is completely non-perf-sensitive, so readability is more important than 20ns. Nanosecond-level optimizations which degrade readability really belong only in hot paths where they have a demonstrable impact on end performance.

@ArminShoeibi
Copy link

@ArminShoeibi your link seems to point to a Java-related question, not .NET.

oh, my bad.

@SergerGood
Copy link
Contributor Author

@SergerGood any specific reason to do this change? Are you seeing performance differences between appending a char and a string?

@roji https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1834

Copy link
Member

@roji roji left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's a quick benchmark:

Method Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated
AppendChar 5.923 ns 0.0093 ns 0.0072 ns - - - -
AppendString 6.682 ns 0.1175 ns 0.0981 ns - - - -

So there's roughly a half-nanosecond difference between these two methods, which doesn't seem significant in any of the code paths being touched. Not all quality rules make sense in all contexts...

We can still do this, but please revert the other changes which remove string interpolation - IMO these just make the code less readable for no reason.

Benchmark code
[MemoryDiagnoser]
public class Program
{
    private StringBuilder _builder;

    [Benchmark]
    public void AppendChar()
    {
        _builder.Clear();
        _builder.Append('x');
    }

    [Benchmark]
    public void AppendString()
    {
        _builder.Clear();
        _builder.Append("X");
    }

    [GlobalSetup]
    public void Setup() => _builder = new StringBuilder();

    static void Main(string[] args)
        => BenchmarkRunner.Run<Program>();
}

@@ -289,13 +289,13 @@ public virtual string Identifier(string name, ICollection<string>? scope = null)

if (partStart != name.Length)
{
builder.Append(name.Substring(partStart));
builder.Append(name, partStart, name.Length - partStart);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason to do this change? The previous code seems simpler.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason is that there is a ready-made native better method for this.

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042
Intel Core i5-9300H CPU 2.40GHz, 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=6.0.100-preview.3.21202.5
  [Host]     : .NET Core 6.0.0 (CoreCLR 6.0.21.20104, CoreFX 6.0.21.20104), X64 RyuJIT
  DefaultJob : .NET Core 6.0.0 (CoreCLR 6.0.21.20104, CoreFX 6.0.21.20104), X64 RyuJIT
Method CharactersCount Mean Error StdDev Median Gen 0 Gen 1 Gen 2 Allocated
StringSubstring 5 23.03 ns 1.147 ns 3.308 ns 21.55 ns 0.0076 - - 32 B
AppendSubstring 5 11.99 ns 0.134 ns 0.119 ns 11.96 ns - - - -
StringSubstring 10 22.92 ns 0.341 ns 0.302 ns 22.81 ns 0.0076 - - 32 B
AppendSubstring 10 11.77 ns 0.216 ns 0.169 ns 11.78 ns - - - -
Benchmark code
  [MemoryDiagnoser]
  public class Program
  {
      private StringBuilder _builder;
      private string _text;
      private int _partStart;
  
      [Params(5, 10)]
      public int CharactersCount { get; set; }
  
      [Benchmark]
      public void StringSubstring()
      {
          _builder.Clear();
          _builder.Append(_text.Substring(_partStart));
      }
  
      [Benchmark]
      public void AppendSubstring()
      {
          _builder.Clear();
          _builder.Append(_text, _partStart, _text.Length - _partStart);
      }
  
      [GlobalSetup]
      public void Setup()
      {
          _builder = new StringBuilder();
          _text = new string('a', CharactersCount);
          _partStart = CharactersCount / 2;
      }
  
      private static void Main() => BenchmarkRunner.Run<Program>();
  }

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I revert using string interpolation.. but there are many examples. And I'm still in favor of uniform approach.

builder.Append("CommandTimeout=").Append(Extension._commandTimeout).Append(' ');

What about Substring in the same file?

builder.Append(name, partStart, i - partStart);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can certainly make things more consistent - but given that nothing here is perf-sensitive, I'd go with what produces the shortest/simplest code, which is string interpolation in most cases (e.g. the CommandTimeout case above).

Accordingly, can you revert this particular change as well? All the rest looks fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Done

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks.

@@ -158,12 +158,12 @@ string ToDebugString(MetadataDebugStringOptions options = MetadataDebugStringOpt
var singleLine = (options & MetadataDebugStringOptions.SingleLine) != 0;
if (singleLine)
{
builder.Append($"Column: {Table.Name}.");
builder.Append("Column: ").Append(Table.Name).Append('.');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please revert

@@ -44,12 +44,12 @@ string ToDebugString(MetadataDebugStringOptions options = MetadataDebugStringOpt
var singleLine = (options & MetadataDebugStringOptions.SingleLine) != 0;
if (singleLine)
{
builder.Append($"FunctionColumn: {Table.Name}.");
builder.Append("FunctionColumn: ").Append(Table.Name).Append('.');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please revert

@@ -44,12 +44,12 @@ string ToDebugString(MetadataDebugStringOptions options = MetadataDebugStringOpt
var singleLine = (options & MetadataDebugStringOptions.SingleLine) != 0;
if (singleLine)
{
builder.Append($"SqlQueryColumn: {Table.Name}.");
builder.Append("SqlQueryColumn: ").Append(Table.Name).Append('.');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please revert

@@ -44,12 +44,12 @@ string ToDebugString(MetadataDebugStringOptions options = MetadataDebugStringOpt
var singleLine = (options & MetadataDebugStringOptions.SingleLine) != 0;
if (singleLine)
{
builder.Append($"ViewColumn: {Table.Name}.");
builder.Append("ViewColumn: ").Append(Table.Name).Append('.');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still doesn't seem like an improvement to me (note that here there are three things, whereas in your two examples there are two). Can you please revert unless there's a good reason to do this?

.AppendLine($"FROM \"{tableName}\"")
.AppendLine($"WHERE {columnType} != 'null'")
.AppendLine($"GROUP BY {columnType}")
.Append("SELECT ").AppendLine(columnType)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please revert

@ghost
Copy link

ghost commented Apr 27, 2021

Hello @roji!

Because this pull request has the auto-merge label, I will be glad to assist with helping to merge this pull request once all check-in policies pass.

p.s. you can customize the way I help with merging this pull request, such as holding this pull request until a specific person approves. Simply @mention me (@msftbot) and give me an instruction to get started! Learn more here.

@ghost
Copy link

ghost commented Apr 27, 2021

Apologies, while this PR appears ready to be merged, I've been configured to only merge when all checks have explicitly passed. The following integrations have not reported any progress on their checks and are blocking auto-merge:

  1. Azure Pipelines

These integrations are possibly never going to report a check, and unblocking auto-merge likely requires a human being to update my configuration to exempt these integrations from requiring a passing check.

Give feedback on this
From the bot dev team

We've tried to tune the bot such that it posts a comment like this only when auto-merge is blocked for exceptional, non-intuitive reasons. When the bot's auto-merge capability is properly configured, auto-merge should operate as you would intuitively expect and you should not see any spurious comments.

Please reach out to us at [email protected] to provide feedback if you believe you're seeing this comment appear spuriously. Please note that we usually are unable to update your bot configuration on your team's behalf, but we're happy to help you identify your bot admin.

@ghost ghost merged commit a2b069c into dotnet:main Apr 27, 2021
@SergerGood SergerGood deleted the using-string-builder branch April 27, 2021 19:11
This pull request was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants