.NET 7 has brought us a lot of great new features and improvements, including some pretty excellent LINQ performance improvements.

To download the source code for this article, you can visit our GitHub repository.

Let’s dive in and see how good the new LINQ actually is.

Setup a Benchmark Project Example

We are going to use BenchmarkDotNet to test the performance so make sure to get it first if you want to test the methods on your machine.

First, let’s set up our test environment by defining our test Student class:

public class Student
{
    public string FirstName { get; set; } = string.Empty;
    public string LastName { get; set; } = string.Empty;
    public int BirthYear { get; set; }
}

Next, we need two lists of 100,000 Student objects and we are going to generate our test data using the Bogus library, along with the two lists of integers derived from the original list:

private readonly Faker<Student> _faker = new Faker<Student>();
private List<Student> students;
private List<Student> students2;
private List<int> studentBirthYears;
private List<int> studentBirthYears2;
private const int count = 100000;

public Benchmark()
{
    students = _faker
        .RuleFor(m => m.FirstName, faker => faker.Person.FirstName)
        .RuleFor(m => m.LastName, faker => faker.Person.LastName)
        .RuleFor(m => m.BirthYear, faker => faker.Person.DateOfBirth.Year)
        .Generate(count);
    students2 = students.ToList();

    studentBirthYears = students.Select(m => m.BirthYear).ToList();
    studentBirthYears2 = studentBirthYears.ToList();
}

We want to analyze .NET 7 LINQ performance in comparison with .NET 6 version.

To do this, we need to make some adjustments to our project file:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFrameworks>net6.0;net7.0</TargetFrameworks>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="BenchmarkDotNet" Version="0.13.2" />
    <PackageReference Include="BenchmarkDotNet.Annotations" Version="0.13.2" />
    <PackageReference Include="Bogus" Version="34.0.2" />
    <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
  </ItemGroup>

</Project>

First, we set the multiple target frameworks in the TargetFrameworks tag (plural).  This enables us to target both .NET 6 and .NET 7.

Finally, we need to set our Benchmark class configuration:

[RankColumn]
[HideColumns(new string[] { "Job" })]
[MemoryDiagnoser(false)]
[SimpleJob(RuntimeMoniker.Net60)]
[SimpleJob(RuntimeMoniker.Net70)]
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByMethod)]
public class Benchmark
{
...
}

Target runtimes are set in the in SimpleJob settings.

Also, we set to group test results by the method in GroupBenchmarksBy setting, add RankColumn, and hide the Job, Error, StdDev, and Median columns to make results a bit clearer.

And now we are finally ready to run our very long test procedure, so you do not need to spend your time on it.

Testing Aggregate Methods

First, we will analyze Min(), Max(), Average(), Count(), and Sum() methods.

Let’s see how these methods perform on a simple list of integers:

|        Method |  Runtime |             Mean | Rank | Allocated |
|-------------- |--------- |-----------------:|-----:|----------:|
|           Min | .NET 6.0 |   656,027.606 ns |    2 |      41 B |
|           Min | .NET 7.0 |    10,243.608 ns |    1 |         - |
|               |          |                  |      |           |
|           Max | .NET 6.0 |   660,551.869 ns |    2 |      41 B |
|           Max | .NET 7.0 |    10,294.283 ns |    1 |         - |
|               |          |                  |      |           |
|       Average | .NET 6.0 |   561,299.193 ns |    2 |      41 B |
|       Average | .NET 7.0 |    13,850.906 ns |    1 |         - |
|               |          |                  |      |           |
|           Sum | .NET 6.0 |   579,482.666 ns |    2 |      41 B |
|           Sum | .NET 7.0 |    34,728.896 ns |    1 |         - |
|               |          |                  |      |           |
|         Count | .NET 6.0 |   569,091.900 ns |    2 |      41 B |
|         Count | .NET 7.0 |    34,944.719 ns |    1 |         - |

Here we can see a significant improvement in the speed of these methods since the .NET 7 version takes only a fraction of the time the .NET 6 version requires.

Even more dramatic is the improvement in memory allocation, where we can see the .NET 7 version uses no memory to perform these tasks. This performance boost is achieved by vectorization of these methods using the Vector<T> class.

A detailed description of how this works is out of the scope of this article and might be covered in one of our future articles.

And now, let’s test them on a list of objects:

|        Method |  Runtime |             Mean | Rank | Allocated |
|-------------- |--------- |-----------------:|-----:|----------:|
|      List_Min | .NET 6.0 | 1,620,622.902 ns |    2 |      44 B |
|      List_Min | .NET 7.0 | 1,348,753.994 ns |    1 |      42 B |
|               |          |                  |      |           |
|      List_Max | .NET 6.0 | 1,260,854.309 ns |    1 |      42 B |
|      List_Max | .NET 7.0 | 1,301,365.550 ns |    2 |      42 B |
|               |          |                  |      |           |
|  List_Average | .NET 6.0 | 1,478,183.348 ns |    2 |      42 B |
|  List_Average | .NET 7.0 | 1,277,449.099 ns |    1 |      42 B |
|               |          |                  |      |           |
|      List_Sum | .NET 6.0 | 1,502,666.745 ns |    2 |      42 B |
|      List_Sum | .NET 7.0 | 1,292,251.353 ns |    1 |      42 B |
|               |          |                  |      |           |
|    List_Count | .NET 6.0 |         4.415 ns |    2 |         - |
|    List_Count | .NET 7.0 |         4.201 ns |    1 |         - |

We can see some improvement in these results, but it is not as significant. 

Speed increases by 10-20% percent, while memory usage is the same in both versions.

An exception from this is the Count() method, which performs extremely fast in both versions. This comes from the properties of the collection of objects and, therefore, is not a .NET 7 improvement.

First() and Last() Methods

Let’s see how First() and Last() methods perform on a list of integers:

|        Method |  Runtime |             Mean | Rank | Allocated |
|-------------- |--------- |-----------------:|-----:|----------:|
|         First | .NET 6.0 |        10.003 ns |    2 |         - |
|         First | .NET 7.0 |         9.271 ns |    1 |         - |
|               |          |                  |      |           |
|          Last | .NET 6.0 |        10.588 ns |    1 |         - |
|          Last | .NET 7.0 |        10.661 ns |    1 |         - |

And on a list of objects:

|        Method |  Runtime |             Mean | Rank | Allocated |
|-------------- |--------- |-----------------:|-----:|----------:|
|    List_First | .NET 6.0 |        11.655 ns |    2 |         - |
|    List_First | .NET 7.0 |         9.678 ns |    1 |         - |
|               |          |                  |      |           |
|     List_Last | .NET 6.0 |        11.163 ns |    2 |         - |
|     List_Last | .NET 7.0 |         9.711 ns |    1 |         - |

We notice about a ~10% speed improvement while no memory allocation is required in both frameworks.

Filtering Methods – Distinct() and Where()

Now, let’s see how filtering performs. For this, we will use methods Distinct() and Where()

In the following examples, we will focus only on the method itself. In other words, we will not use ToList(), ToArray() or any similar method to materialize the results of these methods as you might do in a typical situation.

Again, first, we will see them on a list of integers:

|        Method |  Runtime |             Mean | Rank | Allocated |
|-------------- |--------- |-----------------:|-----:|----------:|
|      Distinct | .NET 6.0 |         7.813 ns |    2 |      64 B |
|      Distinct | .NET 7.0 |         7.369 ns |    1 |      64 B |
|               |          |                  |      |           |
|         Where | .NET 6.0 |        20.730 ns |    2 |      72 B |
|         Where | .NET 7.0 |        17.926 ns |    1 |      72 B |

And on a list of objects:

|        Method |  Runtime |             Mean | Rank | Allocated |
|-------------- |--------- |-----------------:|-----:|----------:|
| List_Distinct | .NET 6.0 |         7.916 ns |    1 |      64 B |
| List_Distinct | .NET 7.0 |         7.569 ns |    1 |      64 B |
|               |          |                  |      |           |
|    List_Where | .NET 6.0 |        17.671 ns |    1 |      72 B |
|    List_Where | .NET 7.0 |        16.282 ns |    1 |      72 B |

Speed improvements are again up to about 10%, while both frameworks allocate the same amount of memory.

Join() Method

Let’s Join() our lists of integers and see how they perform:

|        Method |  Runtime |             Mean | Rank | Allocated |
|-------------- |--------- |-----------------:|-----:|----------:|
|          Join | .NET 6.0 |        18.251 ns |    1 |     160 B |
|          Join | .NET 7.0 |        18.270 ns |    1 |     160 B |

Let’s now Join() our lists of Students:

|        Method |  Runtime |             Mean | Rank | Allocated |
|-------------- |--------- |-----------------:|-----:|----------:|
|     List_Join | .NET 6.0 |        23.167 ns |    1 |     168 B |
|     List_Join | .NET 7.0 |        23.883 ns |    2 |     168 B |

In this example, .NET 7 performs about the same as .NET 6 version.

GroupBy() Method

And finally, let’s analyze the GroupBy() method on a list of integers:

|        Method |  Runtime |             Mean | Rank | Allocated |
|-------------- |--------- |-----------------:|-----:|----------:|
|         Group | .NET 6.0 |         7.899 ns |    2 |      40 B |
|         Group | .NET 7.0 |         7.369 ns |    1 |      40 B |

And on a list of objects:

|        Method |  Runtime |             Mean | Rank | Allocated |
|-------------- |--------- |-----------------:|-----:|----------:|
|    List_Group | .NET 6.0 |         6.345 ns |    1 |      40 B |
|    List_Group | .NET 7.0 |         6.665 ns |    2 |      40 B |

Again, the performance here is pretty much the same as in .NET 6.

Conclusion

In this article, we analyzed the LINQ performance in .NET 7. 

We compared the performance to .NET 6, and we learned how different methods perform on a simple collection of integers and a collection of objects.