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

Looping DbSet.Local is slow in EF Core compared to EF 6 #14231

Closed
HjalteParner opened this issue Dec 21, 2018 · 7 comments · Fixed by #14916
Closed

Looping DbSet.Local is slow in EF Core compared to EF 6 #14231

HjalteParner opened this issue Dec 21, 2018 · 7 comments · Fixed by #14916
Labels
area-perf closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. customer-reported type-enhancement
Milestone

Comments

@HjalteParner
Copy link

I have been queering EF 6 DbSet.Local with great success for a number of years. However doing the same in EF Core is a factor 6 slower which unfortunately is a showstopper for me as I'm dealing with a huge amount of data which again prevent me to make the shift from EF 6 to EF Core.

Please help me to solve this issue.

Below please find an example which can be run both in EF 6. and EF Core

private BlogContext _blogContext;

public void BlogTest()
{
    _logger.Information("BlogTest started");

    _blogContext = new BlogContext();
    _blogContext.ChangeTracker.AutoDetectChangesEnabled = false; // EF Core
    //_blogContext.Configuration.AutoDetectChangesEnabled = false; // EF 6

    // Add blogs to context
    for (int i = 0; i < 10000; i++)
    {
        _blogContext.Blogs.Add(new Blog { ID = i });
    }

    _logger.Information("BlogTest continued");

    // Loop blogs in context
    for (int i = 1; i < 100000; i++)
    {
        foreach (var blog in _blogContext.Blogs.Local)
        {
        }
    }

    _logger.Information("BlogTest ended");
}

public class Blog
{
    public int ID { get; set; }
}

public class BlogContext: DbContext
{
    public DbSet<Blog> Blogs { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=Blog;Trusted_Connection=True;MultipleActiveResultSets=True;");
    }
}
@HjalteParner HjalteParner changed the title Looping DbSet.Local is slow Looping DbSet.Local is slow in EF Core compared to EF 6 Jan 3, 2019
@ajcvickers
Copy link
Member

Note for triage: iteration over the local entities is slow, since it is a view over the state manager.

@HjalteParner A workaround that makes the behavior more like EF6 is to convert the LocalView to an ObservableCollection. For example:

var local = context.Blogs.Local.ToObservableCollection();

foreach (var blog in local)
{
}

This is essentially what EF6 does. It has the downside that the creating the ObservableCollection for a large number of entities can be slow, and the ObservableCollection uses memory. It's probably wise to cache the created collection so that overhead of creating it is not incurred every time.

@HjalteParner
Copy link
Author

HjalteParner commented Jan 4, 2019

Thanks you very much @ajcvickers for your reply. Looping the LocalView through the ObservableCollection definitely makes a big difference making EF Core almost on par with the EF6 implementation.

Another thing I was wondering about is that setting the context AutoDetectChangesEnabled = false in my example seems to have no effect in EF Core while in EF 6 not setting this property to false an as such keeping the default true, makes EF 6 terrible slow.

Can you explain this behavior as well?

@ajcvickers
Copy link
Member

@HjalteParner See #14001

@ajcvickers ajcvickers added this to the Backlog milestone Jan 4, 2019
@HjalteParner
Copy link
Author

Thanks @ajcvickers, that explains the current behavior (or lack of behavior you might say) when AutoDetectChangesEnabled is set to true.

@ajcvickers ajcvickers modified the milestones: Backlog, 3.0.0 Mar 4, 2019
@ajcvickers ajcvickers self-assigned this Mar 4, 2019
ajcvickers added a commit that referenced this issue Mar 4, 2019
Part of #8898
Fixes #14231

Investigation into DbSet.Local shows that:
* Iteration over tracked entities had a lot of LINQ code; fixed by un-LINQing
* Multiple composed iterators cause slow-down; fixed by giving LocalView it's only IStateManager method
* Filtering causes slow-down; fixed by splitting storage into multiple dictionaries. Makes lookup for entity instance slightly slower.
* Calculate Count lazily

This means that DbSet.Local iteration is now about 3x faster, although it depends a lot on what is being tracked. Getting an ObservableCollection and then iterating over that _once_ is slower than just iterating over the entries once. Iterating over the ObservableCollection multiple times is still faster than iterating over DbSet.Local multiple times. This seems like a reasonable trade-off. If the application does heavy iteration, then creating a new collection for that seems reasonable.

With regard to #8898, the layering we have still seems correct, so moving forward with the breaking changes there.
@ajcvickers ajcvickers added the closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. label Mar 4, 2019
ajcvickers added a commit that referenced this issue Mar 4, 2019
Part of #8898
Fixes #14231

Investigation into DbSet.Local shows that:
* Iteration over tracked entities had a lot of LINQ code; fixed by un-LINQing
* Multiple composed iterators cause slow-down; fixed by giving LocalView it's only IStateManager method
* Filtering causes slow-down; fixed by splitting storage into multiple dictionaries. Makes lookup for entity instance slightly slower.
* Calculate Count lazily

This means that DbSet.Local iteration is now about 3x faster, although it depends a lot on what is being tracked. Getting an ObservableCollection and then iterating over that _once_ is slower than just iterating over the entries once. Iterating over the ObservableCollection multiple times is still faster than iterating over DbSet.Local multiple times. This seems like a reasonable trade-off. If the application does heavy iteration, then creating a new collection for that seems reasonable.

With regard to #8898, the layering we have still seems correct, so moving forward with the breaking changes there.
ajcvickers added a commit that referenced this issue Mar 5, 2019
Part of #8898
Fixes #14231

Investigation into DbSet.Local shows that:
* Iteration over tracked entities had a lot of LINQ code; fixed by un-LINQing
* Multiple composed iterators cause slow-down; fixed by giving LocalView it's only IStateManager method
* Filtering causes slow-down; fixed by splitting storage into multiple dictionaries. Makes lookup for entity instance slightly slower.
* Calculate Count lazily

This means that DbSet.Local iteration is now about 3x faster, although it depends a lot on what is being tracked. Getting an ObservableCollection and then iterating over that _once_ is slower than just iterating over the entries once. Iterating over the ObservableCollection multiple times is still faster than iterating over DbSet.Local multiple times. This seems like a reasonable trade-off. If the application does heavy iteration, then creating a new collection for that seems reasonable.

With regard to #8898, the layering we have still seems correct, so moving forward with the breaking changes there.
ajcvickers added a commit that referenced this issue Mar 5, 2019
Part of #8898
Fixes #14231

Investigation into DbSet.Local shows that:
* Iteration over tracked entities had a lot of LINQ code; fixed by un-LINQing
* Multiple composed iterators cause slow-down; fixed by giving LocalView it's only IStateManager method
* Filtering causes slow-down; fixed by splitting storage into multiple dictionaries. Makes lookup for entity instance slightly slower.
* Calculate Count lazily

This means that DbSet.Local iteration is now about 3x faster, although it depends a lot on what is being tracked. Getting an ObservableCollection and then iterating over that _once_ is slower than just iterating over the entries once. Iterating over the ObservableCollection multiple times is still faster than iterating over DbSet.Local multiple times. This seems like a reasonable trade-off. If the application does heavy iteration, then creating a new collection for that seems reasonable.

With regard to #8898, the layering we have still seems correct, so moving forward with the breaking changes there.
ajcvickers added a commit that referenced this issue Mar 5, 2019
Part of #8898
Fixes #14231

Investigation into DbSet.Local shows that:
* Iteration over tracked entities had a lot of LINQ code; fixed by un-LINQing
* Multiple composed iterators cause slow-down; fixed by giving LocalView it's only IStateManager method
* Filtering causes slow-down; fixed by splitting storage into multiple dictionaries. Makes lookup for entity instance slightly slower.
* Calculate Count lazily

This means that DbSet.Local iteration is now about 3x faster, although it depends a lot on what is being tracked. Getting an ObservableCollection and then iterating over that _once_ is slower than just iterating over the entries once. Iterating over the ObservableCollection multiple times is still faster than iterating over DbSet.Local multiple times. This seems like a reasonable trade-off. If the application does heavy iteration, then creating a new collection for that seems reasonable.

With regard to #8898, the layering we have still seems correct, so moving forward with the breaking changes there.
@ajcvickers ajcvickers modified the milestones: 3.0.0, 3.0.0-preview4 Apr 15, 2019
@ajcvickers ajcvickers modified the milestones: 3.0.0-preview4, 3.0.0 Nov 11, 2019
@oeramirez
Copy link

Hi. I think this is still happening. I upgraded a project from Microsoft.EntityFrameworkCore 2.1.11 to 3.1.5 and got performance issues getting data from Local. Please find attached results of profiling.

before
after

@Prinsn
Copy link

Prinsn commented Jul 8, 2020

I don't have quality metrics on this, but that appears to be true for me as well.

Moving from 2.2 to 3.1 and a stress test we have that executes with a warning benchmark of above 24 seconds (and passes in 2.2),

Over 1000 entities being added onto the context, the execution of checking 7 local's with .Any() goes from 67ms to 720ms.

The stress test processes 5000 in just this one spot, so the expected total processing time in 2.2 cannot possibly exceed an average of 4ms each, if my math is correct, so even the baseline execution time is slower by an order of magnitude, potentially two, as that 4ms is for the entirety of code execution.

I also appear to have a 3-50 ms drift elsewhere, that's just DbSet.Local.FirstOrDefault(...) 8 times.

All my other timing metrics are showing a delta between timings of 0 ms from initial to 1000th.

EDIT:

Looks like the problem might be https://docs.microsoft.com/en-us/ef/core/what-is-new/ef-core-3.0/breaking-changes#dbe

Related/more precise/else https://entityframeworkcore.com/knowledge-base/61074160/dbset-tentity--local-any---performance-issue-when-upgraging-ef-core-from-2-2-6-to-3-1-3

@ajcvickers
Copy link
Member

Discussion continued on #21564

@ajcvickers ajcvickers removed their assignment Sep 1, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-perf closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. customer-reported type-enhancement
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants