Skip to content

Extract exception throwing to throw helper #1827

@jnyrup

Description

@jnyrup

Is your feature request related to a problem? Please describe.
When using [MapDerivedType] a switch expression is emitted with a throw expression for the default case.
This is necessary for correctness, but the compiler emits quite some IL for creating and throwing the exception.

public class Base { }

public class Derived : Base { }

[Mapper]
public static partial class A
{
    [MapDerivedType<Derived, Derived>]
    public static partial Base Map(Base source);
}

Generates

// <auto-generated />
#nullable enable
public static partial class A
{
    [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "4.2.1.0")]
    public static partial global::Base Map(global::Base source)
    {
        return source switch
        {
            global::Derived x => x,
            _ => throw new global::System.ArgumentException($"Cannot map {source.GetType()} to Base as there is no known derived type mapping", nameof(source)),
        };
    }
}

Describe the solution you'd like
If we extract the throw expression to it's own method Throw the Map method becomes considerably smaller.
Smaller is not always better, if the extracted method is a hot path, as we then do an extra function call.
But for throw expressions we're already about to create and throw an Exception which by far over shadows the cost of the extra function call.
In .NET6 and upwards we can even use [StackTraceHidden] to exclude Throw from the stack trace to prettify it.

public static class B
{
    public static Base Map(Base source)
    {
        return source switch
        {
            Derived x => x,
            _ => Throw(source),
        };

        [StackTraceHidden]
        static Base Throw(Base source) => throw new ArgumentException($"Cannot map {source.GetType()} to Base as there is no known derived type mapping", nameof(source));
    }
}

Additional context
Here's a benchmark of using the proposed throw helper.

| Method | Mean      | Error     | StdDev    | Ratio | RatioSD | Code Size |
|------- |----------:|----------:|----------:|------:|--------:|----------:|
| MapA   | 0.8429 ns | 0.0158 ns | 0.0148 ns |  1.00 |    0.02 |     739 B |
| MapB   | 0.6197 ns | 0.0105 ns | 0.0093 ns |  0.74 |    0.02 |     155 B |
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using Riok.Mapperly.Abstractions;
using System.Diagnostics;

BenchmarkRunner.Run<CloneBenchmarks>();

[DisassemblyDiagnoser(maxDepth: 2)]
public class CloneBenchmarks
{
    public Base Value { get; set; }

    [GlobalSetup]
    public void GlobalSetup()
    {
        Value = new Derived();
    }

    [Benchmark(Baseline = true)]
    public Base MapA() => A.Map(Value);

    [Benchmark]
    public Base MapB() => B.Map(Value);
}

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions