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

Reduce allocations #26

Merged
merged 11 commits into from Apr 27, 2021
Merged

Reduce allocations #26

merged 11 commits into from Apr 27, 2021

Conversation

kostya9
Copy link
Contributor

@kostya9 kostya9 commented Apr 27, 2021

Noticed some low-hanging fruit when working on parsing CLDR.

  • Use StringBuilderPool via Microsoft.Extensions.ObjectPool
  • Add some net5.0+ specific optimizations
  • Do not go through with unescaping if there is nothing to unescape
  • Store Literal InnerText as string
Method Perf Improvement Memory Improvement
FormatWithCache 19% 54%
FormatWithCacheEscaped 8% 46%
FormatWithoutCache 15% 54%
FormatWithoutCacheEscaped 8% 52%

Before

Method Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated
FormatWithCache 3.956 us 0.0553 us 0.0517 us 1.1444 - - 3.52 KB
FormatWithCacheEscaped 3.127 us 0.0619 us 0.0662 us 0.9308 - - 2.85 KB
FormatWithoutCache 7.657 us 0.1434 us 0.1535 us 2.1210 - - 6.52 KB
FormatWithoutCacheEscaped 5.133 us 0.0398 us 0.0372 us 1.3809 - - 4.25 KB

After

Method Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated
FormatWithCache 3.196 us 0.0185 us 0.0173 us 0.5226 - - 1.6 KB
FormatWithCacheEscaped 2.883 us 0.0132 us 0.0123 us 0.4959 - - 1.52 KB
FormatWithoutCache 6.473 us 0.0375 us 0.0351 us 0.9537 - - 2.95 KB
FormatWithoutCacheEscaped 4.710 us 0.0182 us 0.0161 us 0.6561 - - 2.02 KB

Benchmark code

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

    [MemoryDiagnoser]
    [HtmlExporter]
    [CsvExporter]
    public class MessageFormatBenchmark
    {
        private readonly Dictionary<string, object> _params;
        private readonly string _text;
        private readonly string _textEscaped;
        private readonly MessageFormatter _cacheFormatter;
        private readonly MessageFormatter _noCacheFormatter;

        public MessageFormatBenchmark()
        {
            _textEscaped =
                "These '{'count'}' and thoses '{count}' ain''t not escaped, which makes a total of {count, plural, one {a single pair} other {'#'# (=#) pairs}} of escaped braces.";
            _text =
                @"You have {count, plural, 
                            zero {no notifications}
                            one {just one notification}
                            =42 {a universal amount of notifications}
                            other {# notifications}
                      }. Have a nice day!"; ;
            _params = new Dictionary<string, object> { { "count", 2 } };
            _cacheFormatter = new MessageFormatter(true, "ru");
            _noCacheFormatter = new MessageFormatter(false, "ru");
        }

        [GlobalSetup]
        public void SetUp()
        {
            _cacheFormatter.FormatMessage(_text, _params);
            _cacheFormatter.FormatMessage(_textEscaped, _params);
        }

        [Benchmark]
        public string FormatWithCache()
        {
            return _cacheFormatter.FormatMessage(_text, _params);
        }

        [Benchmark]
        public string FormatWithCacheEscaped()
        {
            return _cacheFormatter.FormatMessage(_textEscaped, _params);
        }

        [Benchmark]
        public string FormatWithoutCache()
        {
            return _noCacheFormatter.FormatMessage(_text, _params);
        }

        [Benchmark]
        public string FormatWithoutCacheEscaped()
        {
            return _noCacheFormatter.FormatMessage(_textEscaped, _params);
        }
    }

Copy link
Owner

@jeffijoe jeffijoe left a comment

Choose a reason for hiding this comment

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

This is awesome!

While you are in there, would you mind checking these out? If you look at the PR's Files tab, at the bottom these are shown:

image

@@ -220,86 +220,94 @@ internal string Pluralize(string locale, ParsedArguments arguments, PluralContex
/// <returns>
/// The <see cref="string" />.
/// </returns>
internal string ReplaceNumberLiterals(StringBuilder pluralized, double n)
internal string ReplaceNumberLiterals(string pluralized, double n)
Copy link
Owner

Choose a reason for hiding this comment

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

I wonder, is there a way to use Span for perf here?

Copy link
Contributor Author

@kostya9 kostya9 Apr 27, 2021

Choose a reason for hiding this comment

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

Here, we get the pluralized value from the cached strings, so I think we can't

Copy link
Contributor Author

@kostya9 kostya9 Apr 27, 2021

Choose a reason for hiding this comment

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

I think we can try using Span APIs more via changing for index StringBuilder loops to GetChunks in NET5.0, but that's a sizeable piece of work in itself )

Copy link
Owner

Choose a reason for hiding this comment

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

Ah, so you think GetChunks is faster than an index accessor?

Copy link
Contributor Author

@kostya9 kostya9 Apr 27, 2021

Choose a reason for hiding this comment

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

Yeah, index accessor basically does a lot of duplicating work when iterating through the whole StringBuilder (finds the chunk where the idx is each time)
See here https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Text/StringBuilder.cs#L497

Copy link
Owner

Choose a reason for hiding this comment

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

Ah nice find! If we can run the tests against .NET 5 as well as the one that does not support it, I would not be opposed to having 2 different implementations of the most critical paths. Alternatively, we can just bump support to the lowest target that supports Span (even in its less optimal form which should still be more performant)

Copy link
Contributor Author

@kostya9 kostya9 Apr 27, 2021

Choose a reason for hiding this comment

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

The GetChunks API is available starting from netcoreapp3.0
https://docs.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.getchunks?view=net-5.0

I prefer multitargeting for now, because netstandard2.1 does not have the APIs required, and the only next version after netstandard2.0 (that supports both Mono and CoreCLR) with the APIs is net5.0

Comment on lines +32 to +40
#if NET5_0_OR_GREATER
foreach (var chunk in src.GetChunks())
{
if (chunk.Span.IndexOfAny(chars) != -1)
{
return true;
}
}
#else
Copy link
Owner

Choose a reason for hiding this comment

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

🔥🔥🔥🔥🔥🔥🔥🔥

@@ -18,6 +18,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="5.0.5" />
Copy link
Owner

Choose a reason for hiding this comment

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

I wonder if this is as performant as the Roslyn internal pool 🤔

src/Jeffijoe.MessageFormat/Parsing/LiteralParser.cs Outdated Show resolved Hide resolved
@kostya9
Copy link
Contributor Author

kostya9 commented Apr 27, 2021

Fixed the warning, analyzers are happy now

@jeffijoe jeffijoe merged commit 33f5330 into jeffijoe:master Apr 27, 2021
@jeffijoe
Copy link
Owner

Excellent work @kostya9 !! 🚀

@kostya9 kostya9 deleted the reduce_allocations branch April 27, 2021 15:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants