Search code examples
c#.netperformance

C# Performance in .NET 7.0 is worse than in .NET 6.0 for accessing properties


I changed from .NET 6.0 to .NET 7.0 and found that some of my performance-critical tasks take almost twice as long as with .NET 6.0 (no code changes were made!).

I was able to derive a simple example which shows at least one interesting effect:

using System;
using System.Collections.Generic;
using System.Diagnostics;

public readonly struct MyStruct {
  public int Parent { get; }
  public int Child { get; } 
  public MyStruct(int p, int c) {
    Parent = p;
    Child = c;
  }
}

static class MyProgram {
  
  static int Main(string[] args) {
    Stopwatch stopwatch = new Stopwatch();
    List<MyStruct> list = new() { new(1, 1)};
    stopwatch.Start();
    for (int j = 0; j < 500_000; ++j) {
      for (int i = 0; i < 1000; ++i) {
        int p = list[0].Parent;
        int c = list[0].Child;
      }
    }
    Console.WriteLine(stopwatch.ElapsedMilliseconds);
    return 0;
  }

}

With .NET 6.0 this takes about 240ms, and with .NET 7.0 450ms (so about 41% longer).

As far as I can tell the MSIL code is equivalent, so it seems the JIT compiler does something severely different in .NET 7.0?

Can anyone confirm this or does anybody else experience performance degradation when switching to .NET 7.0 (for .NET 8.0 it's the same by the way)?

This behavior is very fragile: Make a class of the struct, add another property, combine the nested loops into one, etc. and performance is almost the same for both versions.

Instrumentation shows (for .NET 7.0): enter image description here

and for .NET 6.0: enter image description here

So it shows that .NET 6.0 does some optimizations (inlining?) which are not done any more in .NET 7.0.


Solution

  • There's something wrong with your test. Just resorting to the Stopwatch class and running in Debug mode won't get you a meaningful result. There's a lot more that goes into it, such as warming up the JITer. Using a proper benchmarking tool like BenchmarkDotNet reveals that .NET 7 is actually a bit faster at this, and more memory effficient.

    Method Job Runtime Mean Error StdDev Allocated
    MyBenchmark .NET 6.0 .NET 6.0 366.3 ms 7.24 ms 9.15 ms 728 B
    MyBenchmark .NET 7.0 .NET 7.0 320.0 ms 6.09 ms 5.40 ms 388 B

    MyBenchmarks.cs

    using BenchmarkDotNet.Attributes;
    
    namespace HelloConsole;
    
    [SimpleJob(BenchmarkDotNet.Jobs.RuntimeMoniker.Net60)]
    [SimpleJob(BenchmarkDotNet.Jobs.RuntimeMoniker.Net70)]
    [MemoryDiagnoser]
    public class MyBenchmarks
    {
        public readonly struct MyStruct
        {
            public int Parent { get; }
            public int Child { get; }
            public MyStruct(int p, int c)
            {
                Parent = p;
                Child = c;
            }
        }
    
        [Benchmark]
        public void MyBenchmark()
        {        
            List<MyStruct> list = new() { new(1, 1) };
            for (int j = 0; j < 500_000; ++j)
            {
                for (int i = 0; i < 1000; ++i)
                {
                    int p = list[0].Parent;
                    int c = list[0].Child;
                }
            }
        }
    }
    

    Program.cs

    using BenchmarkDotNet.Running;
    using HelloConsole;
    
    var summary = BenchmarkRunner.Run<MyBenchmarks>();
    

    Project file (.csproj)

    <Project Sdk="Microsoft.NET.Sdk">
      <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFrameworks>net6.0;net7.0</TargetFrameworks>
        <ImplicitUsings>enable</ImplicitUsings>
      </PropertyGroup>
      <ItemGroup>
        <PackageReference Include="BenchmarkDotNet" Version="0.13.5" />
      </ItemGroup>
    </Project>