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

Improved performance and memory consumption #9080

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

ivanvyd
Copy link

@ivanvyd ivanvyd commented Jul 24, 2024

  • Replaced LINQ methods with more efficient alternatives.
  • Ensured proper disposal of CancellationTokenSource instances.
  • Optimized string handling by using StringBuilder instead of string concatenation.
Microsoft Reviewers: Open in CodeFlow

Copy link
Member

@ReubenBond ReubenBond left a comment

Choose a reason for hiding this comment

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

Generally, looks good to me, thank you! There are a few places where changes need to be made for correctness. These kinds of PRs are best split up into smaller PRs (eg, one per type of change), and if you use a tool to assist with the changes, please let us know and tell us where you made interventions, since that helps with reviewing.

@ivanvyd
Copy link
Author

ivanvyd commented Jul 24, 2024

Generally, looks good to me, thank you! There are a few places where changes need to be made for correctness. These kinds of PRs are best split up into smaller PRs (eg, one per type of change), and if you use a tool to assist with the changes, please let us know and tell us where you made interventions, since that helps with reviewing.

Hello @ReubenBond, I highly appreciate your feedback!

I've resolved your comments and created #9082 to handle the problem with undisposed objects.

I will take your advice into consideration and follow it in the future. Looking forward for future cooperation. Thank you!

@tchelidze
Copy link

Out of curiosity, if we had replaced the following line

var serviceConfig = parameters.Where(p => p.Contains(ServicePropertyName)).FirstOrDefault();

with

var serviceConfig = parameters.FirstOrDefault(p => p.Contains(ServicePropertyName));

Would we get the same optimization as with Array.Find or Array.Find is still superior to this ?

@ivanvyd
Copy link
Author

ivanvyd commented Aug 3, 2024

Out of curiosity, if we had replaced the following line

var serviceConfig = parameters.Where(p => p.Contains(ServicePropertyName)).FirstOrDefault();

with

var serviceConfig = parameters.FirstOrDefault(p => p.Contains(ServicePropertyName));

Would we get the same optimization as with Array.Find or Array.Find is still superior to this ?

Hello @tchelidze,
thanks for the question!

When comparing in-memory collections, the main differences between .FirstOrDefault() and Array.Find() are as follows:

  • .FirstOrDefault() is an extension method for IEnumerable<T>. It uses a foreach loop and allocates memory for the Enumerator. The decompiled version looks like this:
foreach (TSource sourceItem in sourceCollection)
{
    if (predicate(sourceItem))
        return sourceItem;
}
return default(TSource);
  • Array.Find() operates specifically on arrays and uses a for loop internally. The decompiled version looks like this:
for (int index = 0; index < sourceCollection.Length; ++index)
{
    if (predicate(sourceCollection[index]))
        return sourceCollection[index];
}
return default(TSource);

These different approaches show next benchmark results (with warmup):

BenchmarkDotNet v0.13.10, Windows 11 (10.0.22631.3958/23H2/2023Update/SunValley3)
Intel Core i7-10510U CPU 1.80GHz, 1 CPU, 8 logical and 4 physical cores
.NET SDK 8.0.400-preview.0.24324.5
  [Host]     : .NET 6.0.30 (6.0.3024.21525), X64 RyuJIT AVX2 [AttachedDebugger]
  DefaultJob : .NET 6.0.30 (6.0.3024.21525), X64 RyuJIT AVX2

| Method             | Mean        | Error     | StdDev     | Median      | Gen0   | Allocated |
|------------------- |------------:|----------:|-----------:|------------:|-------:|----------:|
| FirstOrDefault10   |    78.80 ns |  1.511 ns |   2.983 ns |    78.22 ns | 0.0076 |      32 B |
| Find10             |    17.66 ns |  0.400 ns |   1.103 ns |    17.18 ns |      - |         - |
| FirstOrDefault100  |   554.24 ns |  9.811 ns |  18.427 ns |   549.59 ns | 0.0076 |      32 B |
| Find100            |   138.38 ns |  3.218 ns |   9.128 ns |   134.32 ns |      - |         - |
| FirstOrDefault1000 | 4,515.42 ns | 65.601 ns | 114.895 ns | 4,502.85 ns | 0.0076 |      32 B |
| Find1000           | 1,221.08 ns | 23.956 ns |  24.601 ns | 1,211.59 ns |      - |         - |

From a performance perspective, the method you choose has minimal impact on small collections. However, as the size of the collection increases, Array.Find() generally performs better due to its more efficient implementation compared to .FirstOrDefault().

ivanvyd added 4 commits August 3, 2024 23:10
- Replaced LINQ methods with more efficient alternatives.
- Ensured proper disposal of `CancellationTokenSource` instances.
- Optimized string handling by using `StringBuilder` instead of string concatenation.
…cs`, `StatelessWorkerGrainContext.cs`, `ClusterMembershipService.cs`, and `Silo.cs`
@tchelidze
Copy link

Thanks for the comprehensive explanation @ivanvyd

@richardkooiman
Copy link

When comparing in-memory collections, the main differences between .FirstOrDefault() and Array.Find() are as follows:

  • .FirstOrDefault() is an extension method for IEnumerable<T>. It uses a foreach loop and allocates memory for the Enumerator. The decompiled version looks like this:
foreach (TSource sourceItem in sourceCollection)
{
    if (predicate(sourceItem))
        return sourceItem;
}
return default(TSource);
  • Array.Find() operates specifically on arrays and uses a for loop internally. The decompiled version looks like this:
for (int index = 0; index < sourceCollection.Length; ++index)
{
    if (predicate(sourceCollection[index]))
        return sourceCollection[index];
}
return default(TSource);

@ivanvyd I believe this is no longer the case as of .NET 9 (to be released in ~2 weeks) due to a PR on the dotnet runtime repo (merged on July 22nd) optimizing these specific cases. Most, if not all, of the LINQ extensions methods using a predicate to filter items are now span based, which will no longer allocate any memory on the heap when performing the loop.

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.

4 participants