runtime: .Net 7 performance regression with struct fields

Description

I noticed a ~5-15% performance regression when benchmarking my library ProtoPromise in .Net 6 vs .Net 7. I wasn’t sure what the cause could be, so I tried making a simplified benchmark.

public class PromiseBenchmarks
{
    private Promise.Deferred deferred;

    [Benchmark]
    public void PromiseVoid()
    {
        deferred = Promise.NewDeferred();
        deferred.Promise.Forget();
        deferred.Resolve();
        deferred = default;
    }
}

Then I realized I didn’t need the field and used a local instead.

public class PromiseBenchmarks
{
    private Promise.Deferred deferred;

    [Benchmark]
    public void PromiseField()
    {
        deferred = Promise.NewDeferred();
        deferred.Promise.Forget();
        deferred.Resolve();
        deferred = default;
    }

    [Benchmark]
    public void PromiseLocal()
    {
        var deferred = Promise.NewDeferred();
        deferred.Promise.Forget();
        deferred.Resolve();
    }
}

And that yielded these surprising results

Method Runtime Mean Error StdDev Ratio Code Size
PromiseField .NET 6.0 93.05 ns 1.227 ns 1.148 ns 1.00 448 B
PromiseField .NET 7.0 100.94 ns 2.033 ns 2.342 ns 1.08 425 B
PromiseLocal .NET 6.0 97.22 ns 1.916 ns 1.792 ns 1.00 438 B
PromiseLocal .NET 7.0 92.66 ns 1.617 ns 1.513 ns 0.95 361 B

Clearly .Net 7 is generating better code for the actual work, but there’s something weird with using the field rather than local. The Promise.Deferred struct is simply this:

public struct Deferred
{
    internal readonly Internal.PromiseRefBase.DeferredPromise<Internal.VoidResult> _ref; // class reference.
    internal readonly short _promiseId;
    internal readonly int _deferredId;
}

Configuration

I ran this benchmark with BenchmarkDotNet arguments --runtimes net6.0 net7.0 --disasm --disasmDepth 10000

OS is Windows 10 x64 CPU is AMD Phenom II X6 1055T

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Reactions: 1
  • Comments: 16 (10 by maintainers)

Most upvoted comments

Results with 8.0 preview 5

Method Runtime Mean Error StdDev Ratio Code Size
PromiseField .NET 6.0 94.92 ns 1.167 ns 1.091 ns 1.00 448 B
PromiseField .NET 7.0 104.39 ns 1.926 ns 1.707 ns 1.10 425 B
PromiseField .NET 8.0 81.05 ns 1.424 ns 1.332 ns 0.85 825 B
PromiseLocal .NET 6.0 92.49 ns 0.241 ns 0.201 ns 1.00 438 B
PromiseLocal .NET 7.0 91.98 ns 1.548 ns 1.448 ns 1.00 361 B
PromiseLocal .NET 8.0 76.41 ns 0.095 ns 0.080 ns 0.83 784 B

It looks like performance is much better in .Net 8, but still weirdly slower in .Net 7 with the field. I expect it’s CPU related. Maybe it’s not even worth looking into though since it’s apparently already solved in 8.

@timcassell Kinda offtopic but cheers for Phenom 😄 Had x4 955 and then 8320 FX that somehow happened to be an overcloking beast (5.2ghz @ 1.55v and crazy 2800 nb freq)

@En3Tho Nice. I never upgraded to bulldozer since I heard it was worse than Phenom in some cases, and then just kinda stuck with it. But this thing is really starting to show its age!

@timcassell Kinda offtopic but cheers for Phenom 😄 Had x4 955 and then 8320 FX that somehow happened to be an overcloking beast (5.2ghz @ 1.55v and crazy 2800 nb freq)