Exception handling performance improvements in .NET 9 (Preview 3+) versus .NET 8




Date Added (UTC):

13 Apr 2024 @ 00:33

Date Updated (UTC):

13 Apr 2024 @ 00:33


.NET Version(s):

.NET 8 .NET 9

Tag(s):

#.Net9PerfImprovement


Added By:
Profile Image

Blog   
Ireland    
.NET Developer and tech lead from Ireland!

Benchmark Results:





Benchmark Code:



using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Reports;
using BenchmarkDotNet.Environments;

namespace ExceptionBenchmark
{
    [Config(typeof(Config))]
    [HideColumns(Column.Job, Column.RatioSD, Column.AllocRatio, Column.Gen0, Column.Gen1)]
    [MemoryDiagnoser]
    public class ExceptionBenchmark
    {
        private const int NumberOfIterations = 1000;

        [Benchmark]
        public void ThrowAndCatchException()
        {
            for (int i = 0; i < NumberOfIterations; i++)
            {
                try
                {
                    ThrowException();
                }
                catch
                {
                    // Exception caught - the cost of this is what we're measuring
                }
            }
        }

        private void ThrowException()
        {
            throw new System.Exception("This is a test exception.");
        }

        private class Config : ManualConfig
        {
            public Config()
            {
                AddJob(Job.Default.WithId(".NET 8").WithRuntime(CoreRuntime.Core80).AsBaseline());
                AddJob(Job.Default.WithId(".NET 9").WithRuntime(CoreRuntime.Core90));

                SummaryStyle =
                    SummaryStyle.Default.WithRatioStyle(RatioStyle.Percentage);
            }
        }
    }
}

// .NET 8
public void ThrowAndCatchException()
{
    int num = 0;
    while (num < 1000)
    {
        try
        {
            ThrowException();
        }
        catch
        {
        }
        num++;
    }
}

// .NET 8
.method public hidebysig 
    instance void ThrowAndCatchException () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 1c 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x2050
    // Code size 29 (0x1d)
    .maxstack 2
    .locals init (
        [0] int32 i
    )

    // sequence point: (line 31, col 18) to (line 31, col 27) in _
    IL_0000: ldc.i4.0
    IL_0001: stloc.0
    // sequence point: hidden
    IL_0002: br.s IL_0014
    // loop start (head: IL_0014)
        // sequence point: hidden
        IL_0004: nop
        .try
        {
            // sequence point: (line 35, col 21) to (line 35, col 38) in _
            IL_0005: ldarg.0
            IL_0006: call instance void ExceptionBenchmark.ExceptionBenchmark::ThrowException()
            // sequence point: (line 36, col 17) to (line 36, col 18) in _
            IL_000b: leave.s IL_0010
        }

// .NET 8 (X64)
ThrowAndCatchException()
    L0000: push rbp
    L0001: sub rsp, 0x30
    L0005: lea rbp, [rsp+0x30]
    L000a: mov [rbp-0x10], rsp
    L000e: mov [rbp+0x10], rcx
    L0012: xor eax, eax
    L0014: mov [rbp-4], eax
    L0017: call ExceptionBenchmark.ExceptionBenchmark.ThrowException()
    L001c: int3
    L001d: mov eax, [rbp-4]
    L0020: inc eax
    L0022: mov [rbp-4], eax
    L0025: cmp dword ptr [rbp-4], 0x3e8
    L002c: mov rcx, [rbp+0x10]
    L0030: jl short L0017
    L0032: add rsp, 0x30
    L0036: pop rbp
    L0037: ret
    L0038: push rbp
    L0039: sub rsp, 0x30
    L003d: mov rbp, [rcx+0x20]
    L0041: mov [rsp+0x20], rbp
    L0046: lea rbp, [rbp+0x30]
    L004a: lea rax, [L001d]
    L0051: add rsp, 0x30
    L0055: pop rbp
    L0056: ret


Benchmark Description:


PR -> https://github.com/dotnet/runtime/pull/88034 Looks like MS have ***ported NativeAOT exception handling to CoreCLR***. GH issue thread looking at how performance would be tested -> https://github.com/dotnet/runtime/issues/77568#issuecomment-1453934050

The provided benchmark setup is designed to measure the performance implications of throwing and catching exceptions in .NET applications. It uses BenchmarkDotNet, a powerful .NET library for benchmarking, to conduct and report on these measurements. Let's break down the setup and the rationale behind the benchmark method. ### General Setup - **.NET Version:** The benchmark configuration specifies two .NET versions, ".NET 8" and ".NET 9", using the `CoreRuntime.Core80` and `CoreRuntime.Core90` identifiers. This suggests that the benchmark aims to compare the performance of exception handling between these two future versions of .NET, assuming the code snippet is forward-looking as of my last update in 2023. - **Configuration:** The `Config` class extends `ManualConfig` from BenchmarkDotNet, setting up two jobs for the two .NET versions and marking the ".NET 8" job as the baseline. This means results from ".NET 9" will be compared against ".NET 8" to see if there are any performance improvements or regressions. - **Memory Diagnoser:** The `[MemoryDiagnoser]` attribute is used to collect and report memory allocation statistics, which is crucial for understanding the memory overhead associated with exception handling. - **Columns:** The benchmark hides several columns (Job, RatioSD, AllocRatio, Gen0, Gen1) to focus on the most relevant metrics for this test. ### Benchmark Method: `ThrowAndCatchException` - **Purpose:** This method is designed to measure the performance cost of throwing and catching exceptions in a tight loop. It does this by repeatedly throwing an exception inside a try block and catching it immediately in a catch block, for a total of `NumberOfIterations` times (1000 in this case). - **Performance Aspect:** The benchmark specifically tests the overhead associated with the exception throwing and catching mechanism in .NET. This includes the time it takes to create, throw, and catch an exception, as well as any additional memory allocations that occur as a result. - **Why It's Important:** Exception handling is a common pattern in software development, but it's known to be relatively expensive in terms of performance. Understanding the cost of exceptions can help developers make informed decisions about when and how to use them. For example, in performance-critical paths, developers might opt to use error codes or other mechanisms instead of exceptions. - **Expected Results/Insights:** From running this benchmark, one would expect to gain insights into how exception handling performance might have changed between ".NET 8" and ".NET 9". If the performance improves in ".NET 9", it could indicate optimizations in the runtime's exception handling mechanism. Conversely, if performance regresses, it might warrant further investigation or consideration from developers relying heavily on exceptions. Additionally, the memory diagnostics can reveal if there's a significant difference in memory allocation behavior between the two versions, which could be just as crucial for applications where memory efficiency is a priority. In summary, this benchmark setup and method provide a focused way to assess the performance implications of exception handling across different versions of the .NET runtime, offering valuable insights for developers looking to optimize their applications.


Benchmark Comments: