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

Removed OfType usage. Less allocations and much faster #29962

Closed
wants to merge 2 commits into from
Closed

Removed OfType usage. Less allocations and much faster #29962

wants to merge 2 commits into from

Conversation

ShreyasJejurkar
Copy link
Contributor

@ShreyasJejurkar ShreyasJejurkar commented Feb 7, 2021

The following benchmark test shows usage of OfType extension method can be slower and allocates double memory as compared to good old foreach even on primitive types like string and int.

This PR removes one of the usages of OfType in the method, which gets called by many other extension methods internally like AddMvcCore and AddControllers etc.

    [MemoryDiagnoser]
    public class OfTypeBenchMark
    {
        private static IList<Type> types = new List<Type> { typeof(string), typeof(int) };

        [Benchmark]
        public bool WithOfTypeExtensionMethod()
        {
            return types.OfType<string>().Any();
        }

        [Benchmark]
        public bool WithoutOfType()
        {
            var result = false;

            foreach(var type in types)
            {
                if(type is string)
                {
                    result = true;
                    break;
                }
            }

            return result;
        }
    }
|                    Method |      Mean |    Error |   StdDev |  Gen 0 | Gen 1 | Gen 2 | Allocated |
|-------------------------- |----------:|---------:|---------:|-------:|------:|------:|----------:|
| WithOfTypeExtensionMethod | 106.78 ns | 0.356 ns | 0.316 ns | 0.0612 |     - |     - |      96 B |
|             WithoutOfType |  41.58 ns | 0.117 ns | 0.104 ns | 0.0255 |     - |     - |      40 B |

Contributes to dotnet/runtime#44598

@ghost ghost added the area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates label Feb 7, 2021
@En3Tho
Copy link

En3Tho commented Feb 7, 2021

I don't think it's necessary to replace easily readable Linq with manual foreach in methods that run only once. I doubt it will bring any noticeable difference. I believe that issue you've linked is about optimizing core things, but I might be wrong.

@ShreyasJejurkar
Copy link
Contributor Author

LINQ brings readability, but it lowers the performance! And this method is get's called at startup, and we want to make startup as fast as possible!
Some more info -> dotnet/runtime#45060

@AmrAlSayed0
Copy link

AmrAlSayed0 commented Feb 7, 2021

You're benchmark is wrong. You're list has objects of type Type and you're trying to cast it to string in the first test.

The second test just checks for equality which is not the same thing.

@BrennanConroy BrennanConroy added the community-contribution Indicates that the PR has been added by a community member label Feb 7, 2021
Corrected the `if` condition.
Comment on lines +68 to +79
var controllerFeaturePresent = false;

foreach(var feature in manager.FeatureProviders)
{
if(feature is ControllerFeatureProvider)
{
controllerFeaturePresent = true;
break;
}
}

if(!controllerFeaturePresent)
Copy link
Member

Choose a reason for hiding this comment

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

You should be able to get exactly the same performance from this:

Suggested change
var controllerFeaturePresent = false;
foreach(var feature in manager.FeatureProviders)
{
if(feature is ControllerFeatureProvider)
{
controllerFeaturePresent = true;
break;
}
}
if(!controllerFeaturePresent)
if(!manager.FeatureProviders.Any(provider => provider is ControllerFeatureProvider))

Copy link
Member

@stephentoub stephentoub Feb 10, 2021

Choose a reason for hiding this comment

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

exactly the same performance

It won't be "exactly" the same: every element would now involve a delegate invocation.

But I don't think that matters here. I agree with Sam we should keep this simple and that this is a perfectly reasonable place to use Enumerable.Any(func). I would change my tune if this were part of a default Blazor wasm app and was the last thing keeping Enumerable.Any alive in such an app and we could otherwise trim out Enumerable.Any, but it's not.

@javiercn
Copy link
Member

I agree with @stephentoub that the code is fine as it is and we don't need to change it unless we see significant performance benefits. I believe there should be other areas we should look at first before going for these micro optimizations.

@ShreyasJejurkar
Copy link
Contributor Author

My intension is to remove out slow things that impacts startup. I know resulting code is not quite readable, but also it's not that much hard to understand! 😄

@javiercn
Copy link
Member

My intension is to remove out slow things that impacts startup.

What's the improvement over the overall startup time?

@sharwell
Copy link
Member

@javiercn would you agree that the alternative form I proposed is equally readable to the original?

@javiercn
Copy link
Member

@javiercn would you agree that the alternative form I proposed is equally readable to the original?

Yes, I'm not really complaining about any of these being less readable, I'm suggesting that maybe is not worth changing given that I don't have a clear idea of how much improvement this brings to the overall startup experience (I see from the issue linked that there is a general effort to improve this area in general).

I'm just pointing out that I don't know the impact this has in total startup time, but I suspect is negligible, and at that point, is it worth changing the code?

The risk here is the chance of introducing a small/subtle behavior change/bug, if we are taking that risk, at least we should make sure is worth it.

Again, I'm happy if we take this PR in whatever shape provided the implementation remains equivalent, but for future reference we should at least establish a minimal threshold to determine what's worth optimizing.

Does that make sense?

@sharwell
Copy link
Member

Does that make sense?

Certainly, was just getting clarification on the reason for wanting more data. 👍

@MCCshreyas if you want to capture more data about this, one of the easiest things to capture is a count of the number of times this method is called during the startup process. If the count varies according to the program configuration, then a set of minimum/expected/maximum counts would be particularly helpful.

@pranavkm
Copy link
Contributor

f you want to capture more data about this, one of the easiest things to capture is a count of the number of times this method is called during the startup process

In the templates, it's usually invoked once.

@pranavkm
Copy link
Contributor

@MCCshreyas thanks for your PR. I think we need to be able to notice measurable startup improvements for changes like this. Unfortunately microbenchmarks might not necessarily demonstrate this. I'm closing this. If you get the opportunity to measure startup perf (https://github.com/dotnet/crank has features that can let you do this), we'd be happy to reconsider this change.

@pranavkm pranavkm closed this Feb 19, 2021
@ShreyasJejurkar ShreyasJejurkar deleted the linq-usage branch April 13, 2021 06:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates community-contribution Indicates that the PR has been added by a community member
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants