BenchmarkDotNet and EF Core vs EF6 - Part 2

This is part 2 of a multi-part series on benchmarking EF Core 6 and EF6 using BenchmarkDotNet. In this post I will explore and compare the performance of EF Core and EF 6 for multiple scenarios designed to mock real world use cases.

1. See Introduction to BenchmarkDotNet if you're just getting started on the BenchmarkDotNet journey or looking for Hello World with simple examples.

2. See BenchmarkDotNet and EF Core vs EF6 - Part 1 for the configuration of benchmarks and database setup.

3. For all code in this post and all subsequent posts on EF Core 6 vs EF6 Benchmarking, see the following project on my GitHub at https://github.com/BrianMikinski/EfPerformance

TLDR;

EF Core 6 is fast. Very Fast. Across all benchmark scenarios, you should be using EF Core if performance is critical to your success.

Introduction

These days, EF Core 6 is certainly one of the most exciting ORM libraries on the .Net landscape. Beyond the near feature parity with EF6, the best part of EF Core is that performance continues to improve with each release. The latest EF Core 6 version is no exception to what we’ve come to expect from the EF team – they truly are Magic Unicorns when it comes to making EF Core as fast as it possibly can be.

But how does EF Core 6 stack up against EF6? In this post, I’m going to continue my benchmarking of EF6 and EF Core but this time with more realistic application scenarios. As you probably already know, EF Core continues to blow EF6 out of the water in nearly all aspects of performance but these benchmarks put some solid data behind both technologies.

Real World LINQ Scenarios and EF DbContext Configurations

While the previous EF Core 6 vs EF6 benchmarks focused on simple Hello World examples, this post will attempt to test more real-world scenarios. We will explore the following commonly encountered application requirements of EF Core -

  1. Insert Single Entity
  2. Retrieve Entity, Update and Save
  3. Insert Multiple Entities
  4. EF Extentions Bulk Insert Entities vs AddRange Entities Performance

EF DbContext Configuration

For each application scenario, we will run three separate benchmarks targeting common EF DbContext configurations across EF6 and EF Core 6.

  • EF6
    • The Baseline EF6 configuration. What you would expect to see in a classic .Net Framework application using Entity Framework.
  • EF Core
    • Most common EF Core 6 DbContext configuration. Nearly identical to the EF6 .Net Framework setup except primarily for .Net Core and .Net 5+ applications.
  • EF Core Pooled
    • A new feature as of EF Core 2.0, Pooled DbContexts provide many benefits to performance from both a memory and CPU perspective. As you will see, they are the DbContext that everyone should be using. The only downside to Pooled Ef Core DbContexts is that they cannot be used if your DbContext manages internal state via private fields. For more information see the Limitations section of What Is New and Advanced Performance Topics

Insert Single Entity

What are we benchmarking?

One of the most classic Entity Framework app scenarios, in this LINQ command we will be inserting a single Post entity into the database using DbContext.Add(). As you will see, EF6 handles this type of operation fairly reasonably but when we get to EF Core, the Pooled Db Contexts really start to shine from both a CPU and Memory perspective. This same pattern will begin to develop for nearly all of our benchmarks.

AddSingleEntityBenchmark.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
using BenchmarkDotNet.Attributes;
using Blog.Models;
using CoreBlog.Models;

namespace Blog.Benchmarks;

public class AddSingleEntityBenchmark : BenchmarkBase
{
    [GlobalSetup]
    public void GlobalSetup()
    {
        ConfigDatabases();
    }

    [Benchmark(Baseline = true)]
    public void Ef6()
    {
        using var context = new BlogContext();

        var post = Post.NewPost();

        context.Posts.Add(post);
        context.SaveChanges();
    }

    [Benchmark]
    public void EfCore()
    {
        using var context = new CoreBlogContext(CoreBlogContext.NewDbContextOptions());

        var post = PostCore.NewPost();

        context.Posts.Add(post);
        context.SaveChanges();
    }

    [Benchmark]
    public void EfCorePooled()
    {
        using var context = _corePooledDbContextFactory.CreateDbContext();

        var post = PostCore.NewPost();

        context.Posts.Add(post);
        context.SaveChanges();
    }
}

 

Results

1
2
3
4
5
6
7
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19043.1826 (21H1/May2021Update)
Intel Core i5-6200U CPU 2.30GHz (Skylake), 1 CPU, 4 logical and 2 physical cores
.NET SDK=6.0.302
  [Host]     : .NET 6.0.7 (6.0.722.32202), X64 RyuJIT
  Job-IDQVNR : .NET 6.0.7 (6.0.722.32202), X64 RyuJIT

WarmupCount=1  

 

Method Mean Error StdDev Min Max Ratio RatioSD Gen 0 Allocated
Ef6 12.471 ms 0.8909 ms 2.627 ms 5.419 ms 18.03 ms 1.00 0.00 93.7500 155 KB
EfCore 6.356 ms 1.1345 ms 3.237 ms 1.492 ms 15.70 ms 0.54 0.33 - 89 KB
EfCorePooled 5.428 ms 1.0484 ms 3.025 ms 1.059 ms 14.61 ms 0.46 0.30 - 22 KB

 

Blog.Benchmarks.AddSingleEntityBenchmark-barplot.webp
Blog.Benchmarks.AddSingleEntityBenchmark-boxplot.webp

Summary

Method Mean Ratio Improvement Allocated Improvement
Ef6 12.471 ms 1.00 - 155 KB -
EfCore 6.356 ms 0.54 1.96x 89 KB 1.74x
EfCorePooled 5.428 ms 0.46 2.29x 22 KB 7.04x

 

Analysis

While the roughly 2x increase in CPU and Memory allocation efficiency between EF6 and EF Core is a nice improvement, the EF Core Pooled DbContext really takes the cake. Coming in at a ~2.3x increase in CPU efficiency and 86% decrease in allocated KB, this is the DbContext you should be using. Why is the EF Core Pooled DbContext faster than the non pooled context and why is the allocated memory so much less? The enhanced performance is the result of the skipping the startup configuration of the internal EF Core DbContext services.

How does this work? DbContextFactory initially configures a DbContext that is reset at every call to _corePooledDbContextFactory.CreateDbContext(). For more information related to configuring DbContextFactories, see previous blog posts Introduction to BenchmarkDotNet and BenchmarkDotNet and EF Core vs EF6 - Part 1.

Retrieve, Update and Save

What are we benchmarking?

Another very common scenario in real world applications, in this benchmark, we want to analyze

  1. Retrieving a single entity with DbContext.FirstOrDefault()
  2. Updating a property on the entity with post?.UpdateTitle("Is this faster than EF Core") and
  3. Saving the updated post title back to the database with DbContext.SaveChanges();

FirstOrDefaultUpdateBenchmark.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
using BenchmarkDotNet.Attributes;
using Blog.Models;
using CoreBlog.Models;

namespace Blog.Benchmarks;

public class FirstOrDefaultUpdateBenchmark : BenchmarkBase
{
    [GlobalSetup]
    public void GlobalSetup()
    {
        ConfigDatabases();
        AddPostsToSeedLimit();
    }

    [Benchmark(Baseline = true)]
    public void Ef6()
    {
        using var context = new BlogContext();
        
        var post = context.Posts.FirstOrDefault();
        post?.UpdateTitle("Is this faster than EF Core");

        context.SaveChanges();
    }


    [Benchmark]
    public void EfCore()
    {
        using var context = new CoreBlogContext(CoreBlogContext.NewDbContextOptions());

        var post = context.Posts.FirstOrDefault();
        post?.UpdateTitle("EF Core will Rock your socks off!");

        context.SaveChanges();
    }

    [Benchmark]
    public void EfCorePooled()
    {
        using var context = _corePooledDbContextFactory.CreateDbContext();
        
        var post = context.Posts.FirstOrDefault();
        post?.UpdateTitle("EF Core will Rock your socks off!");

        context.SaveChanges();
    }
}

 

Results

1
2
3
4
5
6
7
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19043.1826 (21H1/May2021Update)
Intel Core i5-6200U CPU 2.30GHz (Skylake), 1 CPU, 4 logical and 2 physical cores
.NET SDK=6.0.302
  [Host]     : .NET 6.0.7 (6.0.722.32202), X64 RyuJIT
  Job-ZWIYRJ : .NET 6.0.7 (6.0.722.32202), X64 RyuJIT

WarmupCount=1  

 

Method Mean Error StdDev Median Min Max Ratio RatioSD Gen0 Allocated Alloc Ratio
Ef6 8,641.5 μs 430.79 μs 1,207.98 μs 8,234.9 μs 6,681.9 μs 12,181.9 μs 1.00 0.00 125.0000 194.03 KB 1.00
EfCore 917.6 μs 37.21 μs 106.16 μs 906.4 μs 632.8 μs 1,186.0 μs 0.11 0.02 46.8750 73.93 KB 0.38
EfCorePooled 332.3 μs 15.99 μs 47.15 μs 321.6 μs 262.8 μs 438.5 μs 0.04 0.01 5.3711 8.81 KB 0.05
Blog.Benchmarks.FirstOrDefaultUpdateBenchmark-barplot.webp
Blog.Benchmarks.FirstOrDefaultUpdateBenchmark-boxplot.webp

 

Summary

Method Mean Ratio Improvement Allocated Improvement
Ef6 8,641.5 μs 1.00 - 194.03 KB -
EfCore 917.6 μs 0.11 9.75x 73.93 KB 1.74
EfCorePooled 332.3 μs 0.04 26.00x 8.81 KB 19.40x

 

Analysis

Once again, the EF Core Pooled DbContext takes the cake. Clocking in at a 26x CPU performance improvement and 19x improvement in allocated bytes from the EF6 DbContext, this context is the winner. The traditional EF Core DbContext still performed very well but the Pooled DbContext wins out overall. Let’s see what happens next when we start to insert multiple entities in one EF command…

Insert Multiple Entities

What are we benchmarking?

When building real world applications, engineers will often need to insert multiple entities in a single EF LINQ statement. Traditionally, EF6 has not performed well in these scenarios and as you’ll see, it definitely struggles against EF Core. This benchmark uses DbContext.AddRange() to add and track multiple entities against the database and like all other entity updates, all create, update, and delete changes are saved to the database using DbContext.SaveChages()

FirstOrDefaultUpdateBenchmark.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
using BenchmarkDotNet.Attributes;
using Blog.Models;
using CoreBlog.Models;

namespace Blog.Benchmarks;

public class AddMultipleEntitiesBenchmark : BenchmarkBase
{
    [GlobalSetup]
    public void GlobalSetup()
    {
        ConfigDatabases();
    }

    [Benchmark(Baseline = true)]
    public void Ef6()
    {
        using var context = new BlogContext();

        for (int i = 0; i < SeedLimit; i++)
        {
            context.Posts.Add(new Post());
        }

        context.SaveChanges();
    }

    [Benchmark]
    public void EfCore()
    {
        using var context = new CoreBlogContext(CoreBlogContext.NewDbContextOptions());

        for (int i = 0; i < SeedLimit; i++)
        {
            context.Posts.Add(new PostCore());
        }

        context.SaveChanges();
    }

    [Benchmark]
    public void EfCorePooled()
    {
        using var context = _corePooledDbContextFactory.CreateDbContext();

        for (int i = 0; i < SeedLimit; i++)
        {
            context.Posts.Add(new PostCore());
        }

        context.SaveChanges();
    }
}

Results

1
2
3
4
5
6
7
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19043.1826 (21H1/May2021Update)
Intel Core i5-6200U CPU 2.30GHz (Skylake), 1 CPU, 4 logical and 2 physical cores
.NET SDK=6.0.302
  [Host]     : .NET 6.0.7 (6.0.722.32202), X64 RyuJIT
  Job-YWRNBA : .NET 6.0.7 (6.0.722.32202), X64 RyuJIT

WarmupCount=1  

 

Method Mean Error StdDev Median Min Max Ratio Gen 0 Gen 1 Gen 2 Allocated
Ef6 6,819.43 ms 290.312 ms 842.249 ms 7,044.13 ms 5,222.54 ms 8,392.86 ms 1.000 516000.0000 11000.0000 1000.0000 804 MB
EfCore 76.39 ms 4.655 ms 13.506 ms 69.15 ms 60.42 ms 110.74 ms 0.011 1000.0000 - - 10 MB
EfCorePooled 65.12 ms 1.502 ms 4.009 ms 64.18 ms 59.75 ms 80.32 ms 0.009 1000.0000 - - 10 MB
Blog.Benchmarks.AddMultipleEntitiesBenchmark-barplot.webp
Blog.Benchmarks.AddMultipleEntitiesBenchmark-boxplot.webp

 

Summary

Method Mean Ratio Improvement Allocated Improvement
Ef6 6,819.43 ms 1.000 - 804 MB -
EfCore 76.39 ms 0.011 89.27x 10 MB 80.40x
EfCorePooled 65.12 ms 0.009 104.72x 10 KB 80.40x

 

Analysis

Although we often want to avoid it, EF DbContext’s can sometimes get trashed. The longer they live, the more items that get added causing additional allocations and increased CPU time to SaveChanges() to the database. The benchmark perfectly demonstrates the performance implications of such DbContext bloat and how it is handled in EF Core and EF6 during SaveChanges(). With 104x improvement in CPU time and a 80.40x improvement to the allocated bytes, EF Core certainly performs better than EF6. When engineers inevitable encounter long lived DbContext’s EF Core will out perform EF6.

Bulk Insert Entities

What are we testing?

Up until this point, all benchmarks have used vanilla EF Core 6 or EF6 libraries from Microsoft with no additional cruft. We are finally going to change that and utilize the very popular and very helpful extension from the folks at https://entityframework-extensions.net/ that allow engineers to bypass EF Change Tracking and SaveChanges() to bulk insert entities as rapidly as possible. Over the years I have found these extensions to be invaluable and worth ever penny for a license. Additionally, you can test the extensions out for free just by installing their NuGet hosted package. In this benchmark, we will be comparing the standard EF LINQ query DbContext.AddRange() coupled with DbContext.SaveChanges(), to the DbContext.BulkInsert(entities) extension.

BulkInsertEntitiesBenchmark.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
using BenchmarkDotNet.Attributes;
using Blog.Models;
using CoreBlog.Models;

namespace Blog.Benchmarks;

public class BulkInsertEntitiesBenchmark : BenchmarkBase
{
    [GlobalSetup]
    public void GlobalSetup()
    {
        ConfigDatabases();
    }

    [Benchmark(Baseline = true)]
    public void Ef6AddRange()
    {
        using var context = new BlogContext();

        List<Post> posts = new();

        for (int i = 0; i < SeedLimit; i++)
        {
            posts.Add(new Post());
        }

        context.Posts.AddRange(posts);
        context.SaveChanges();
    }

    [Benchmark]
    public void Ef6BulkInsertEfExtensions()
    {
        using var context = new BlogContext();

        List<Post> posts = new();

        for (int i = 0; i < SeedLimit; i++)
        {
            posts.Add(new Post());
        }

        context.BulkInsert(posts);
    }

    [Benchmark]
    public void EfCoreAddRange()
    {
        using var context = _corePooledDbContextFactory.CreateDbContext();

        List<PostCore> posts = new();

        for (int i = 0; i < SeedLimit; i++)
        {
            posts.Add(new PostCore());
        }

        context.Posts.AddRange(posts);
        context.SaveChanges();
    }

    [Benchmark]
    public void EfCoreBulkInsertEfExtensions()
    {
        using var context = _corePooledDbContextFactory.CreateDbContext();

        List<PostCore> posts = new();

        for (int i = 0; i < SeedLimit; i++)
        {
            posts.Add(PostCore.NewPost());
        }

        context.BulkInsert(posts);
    }
}

Results

1
2
3
4
5
6
7
BenchmarkDotNet=v0.13.2, OS=Windows 10 (10.0.19043.1889/21H1/May2021Update)
Intel Core i5-6200U CPU 2.30GHz (Skylake), 1 CPU, 4 logical and 2 physical cores
.NET SDK=6.0.303
  [Host]     : .NET 6.0.8 (6.0.822.36306), X64 RyuJIT AVX2
  Job-TYIMDK : .NET 6.0.8 (6.0.822.36306), X64 RyuJIT AVX2

WarmupCount=1  

 

Method Mean Error StdDev Median Min Max Ratio Gen0 Gen1 Gen2 Allocated Alloc Ratio
Ef6AddRange 3,477.80 ms 66.229 ms 173.310 ms 3,450.21 ms 3,066.94 ms 4,056.29 ms 1.000 21000.0000 5000.0000 1000.0000 64713.64 KB 1.00
Ef6BulkInsertZzzEfExtensions 33.89 ms 1.209 ms 3.508 ms 32.79 ms 28.75 ms 43.04 ms 0.010 285.7143 - - 1244.82 KB 0.02
EfCoreAddRange 66.77 ms 1.688 ms 4.648 ms 65.05 ms 60.94 ms 81.22 ms 0.019 1666.6667 333.3333 - 10319.63 KB 0.16
EfCoreBulkInsertZzzEfExtensions 23.27 ms 0.431 ms 0.820 ms 23.42 ms 21.37 ms 24.93 ms 0.007 250.0000 - - 810.9 KB 0.01

 

Blog.Benchmarks.BulkInsertEntitiesBenchmark-barplot.webp
Blog.Benchmarks.BulkInsertEntitiesBenchmark-boxplot.webp

 

Summary

Method Mean Ratio Improvement Allocated Improvement
Ef6AddRange 3,477.80 ms 1.000 - 64,713.64 KB -
Ef6BulkInsertEfExtensions 33.89 ms 0.010 102.59x 12,44.82 KB 51.59x
EfCoreAddRange 66.77 ms 0.019 52.08x 103,19.63 KB 6.27x
EfCoreBulkInsertEfExtensions 23.27 ms 0.007 2.87x, 149.45x 810.9 KB 1.72x, 79.89x

 

Analysis

Of all the benchmarks covered in this post, this one is by far my favorite. Look at the incredible difference between the EF6 AddRange() to the EF6 BulkInsert() extensions? A massive 102x improvement to the execution speed of inserting 1000 entities. Now compare the EF6 DbContext.AddRange() to the EF Core Pooled BulkInserts extension – an even more massive 149x execution speed improvement and a 79x reduction in your memory footprint! If ever anyone needed backup to support a migration away from EF6 to EF 6 Core, this is it. I would also add, if you can’t move your app to EF Core and you do a large amount of bulk inserts, it is beyond a shadow of a doubt in your best interest to pick up a license of Entity Framework Extensions.

Conclusion

At this point, any concerns realted to the performance benefits of EF Core should be laid to rest. When it comes to both CPU and memory performance, EF Core beats EF6 hands down, no questions asked in all benchmark scenarios. While the enhanced CPU speed benefits will be most noticeable, the reduced memory footprint is equally important. Does it get any better? Well, actually it might - with the release of .Net 7 and EF7 in November of 2022, preliminary benchmarks of the updated EF7 pipeline indicate that for some scenarios, EF Core may be up to 4x faster. Come November, be on the lookout for future blog posts exploring EF7 performance enhancements…..but until then…..Happy Coding!

If you want to view all of the code from this post, you can access the source at my github repo https://github.com/BrianMikinski/EfPerformance


Gravatar Image

Brian Mikinski

Software Architect - Houston, Tx