Skip to content

Latest commit

 

History

History
203 lines (152 loc) · 9.65 KB

0013.create-objects-without-calling-ctor.md

File metadata and controls

203 lines (152 loc) · 9.65 KB

Как я создавал generic-объекты, не вызывая явно их конструктор

Мотивация

В одном из проектов сложилась ситуация, когда есть интерфейс IEvent, несколько классов, его реализующих, и необходимость иногда оборачивать эти ивенты в generic-класс ApplicationEvent:

public interface IEvent {}

public record SomethingCreatedEvent : IEvent;

public record AnotherEvent : IEvent;

public interface INotification {}

public record ApplicationEvent<TEvent>(TEvent Event) : INotification where TEvent : IEvent;

public class Mapper { 
    public static INotification Map(IEvent evt) =>
        evt switch
        {
            SomethingCreatedEvent e => new ApplicationEvent<SomethingCreatedEvent>(e),
            AnotherEvent e => new ApplicationEvent<AnotherEvent>(e),
            _ => null
        };
}

Я захотел не пополнять switch при добавлении каждой новой реализации IEvent, а сделать так, чтобы оно «само как-нибудь».

Делаю подход, получаю:

public static INotification Map(IEvent evt) =>
    Activator.CreateInstance(typeof(ApplicationEvent<>).MakeGenericType(evt.GetType()), evt) as INotification;

Задаюсь вопросом, насколько это производительно. Нахожу статью Benchmarking 4 reflection methods for calling a constructor in .NET от Andrew Lock. Там указано, что CompiledExpression существенно быстрее создания через Activator (без учёта создания и компиляции самого выражения)

Пробую

dotnet new -i BenchmarkDotNet.Templates
mkdir benchmark && cd "$_"
dotnet new benchmark -f net6.0 --console-app

со следующим Benchmarks.cs

namespace benchmark
{
    public interface IEvent {}

    public record SomethingCreatedEvent : IEvent;

    public interface INotification {}

    public record ApplicationEvent<TEvent>(TEvent Event) : INotification where TEvent : IEvent;

    public class Benchmarks
    {
        private readonly IEvent _somethingCreatedEvent = new SomethingCreatedEvent();
        private static readonly Type EventType = typeof(SomethingCreatedEvent);

        private static readonly Delegate CompiledExpressionForGeneric = MakeExpression(EventType);
        private static readonly Type ApplicationEventType =
            typeof(ApplicationEvent<>).MakeGenericType(EventType);

        private static Delegate MakeExpression(Type type)
        {
            var apEventType = typeof(ApplicationEvent<>).MakeGenericType(type);
            var constructorInfos = apEventType.GetConstructor(new[] { type });
            var arg = Expression.Parameter(type, "Event");

            var constructorExpression = Expression.New(constructorInfos, arg);
            return Expression.Lambda(constructorExpression, arg).Compile();
        }

        [Benchmark(Baseline = true)]
        public INotification CreateViaCtor() =>
            new ApplicationEvent<SomethingCreatedEvent>((SomethingCreatedEvent)_somethingCreatedEvent);

        [Benchmark]
        public INotification CreateViaActivator() =>
            Activator.CreateInstance(ApplicationEventType, _somethingCreatedEvent) as INotification;

        [Benchmark]
        public INotification CreateViaExpression() =>
            CompiledExpressionForGeneric.DynamicInvoke(_somethingCreatedEvent) as INotification;
    }
}

Результаты:

BenchmarkDotNet=v0.12.1, OS=macOS 12.6.2 (21G320) [Darwin 21.6.0]
Intel Core i5-8259U CPU 2.30GHz (Coffee Lake), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=6.0.402
  [Host]     : .NET Core 6.0.10 (CoreCLR 6.0.1022.47605, CoreFX 6.0.1022.47605), X64 RyuJIT
  DefaultJob : .NET Core 6.0.10 (CoreCLR 6.0.1022.47605, CoreFX 6.0.1022.47605), X64 RyuJIT
Method Mean Error StdDev Ratio RatioSD
CreateViaCtor 8.383 ns 0.1117 ns 0.0872 ns 1.00 0.00
CreateViaActivator 523.390 ns 3.1727 ns 2.6494 ns 62.43 0.71
CreateViaExpression 528.583 ns 4.2310 ns 3.7506 ns 63.01 0.81

🤔 – выходит, что разницы между созданием через активатор и созданием через выражение особо нет.

Предполагаю, что были какие-то оптимизации и пробую воспроизвести эксперимент от Andrew:

    public class Benchmarks
    {
        private static readonly Type EventType = typeof(SomethingCreatedEvent);

        private static readonly Func<SomethingCreatedEvent> CreateEventExpression =
            Expression.Lambda<Func<SomethingCreatedEvent>>(Expression.New(EventType)).Compile();  

        [Benchmark(Baseline = true)]
        public IEvent CreateEventViaCtor() => new SomethingCreatedEvent();

        [Benchmark]
        public IEvent CreateEventViaActivator() => Activator.CreateInstance(EventType) as IEvent;

        [Benchmark]
        public IEvent CreateEventViaExpression() => CreateEventExpression.Invoke();
    }

Результаты

Method Mean Error StdDev Ratio RatioSD
CreateEventViaCtor 5.367 ns 0.0957 ns 0.0895 ns 1.00 0.00
CreateEventViaActivator 14.607 ns 0.0932 ns 0.0826 ns 2.72 0.06
CreateEventViaExpression 6.014 ns 0.0686 ns 0.0642 ns 1.12 0.02

Сходится, значит, при создании generic-класса происходит что-то особое, что нивелирует разницу между активатором и выражением.

При этом замедление в 60 раз заставляет меня засомневаться в том, что я хочу отказаться от явного создания ApplicationEvent

upd от 2023.01.30:

По наводке от @Pliner порассматривал EasyNetQ/EasyNetQ#1519 и понял, что делаю не так: DynamicInvoke убивает производительность. Не надо пытаться сделать аргумент правильного типа – так как тип этот будет известен только в рантайме, мы не можем обойтись без DynamicInvoke, а если сделать выражение, принимающее object, то всё сойдётся:

код

private static Func<object, INotification> MakeParameterizedLambdaExpression(Type type)
{
    var applicationEventType = typeof(ApplicationEvent<>).MakeGenericType(type);
    var constructorInfo = applicationEventType.GetConstructor(new[] { type });
    var ctorArg = Expression.Parameter(typeof(object), "Event"); // <- здесь изменения относительно первоначального варианта

    var constructorExpression = Expression.New(
        constructorInfo,
        Expression.Convert(ctorArg, type) // <- здесь изменения относительно первоначального варианта
    );
    return Expression
        .Lambda<Func<object, INotification>>(constructorExpression, ctorArg)  // <- параметризованная Lambda
        .Compile();
}

public static readonly Func<object, INotification> CompiledParameterizedExpression =
    MakeParameterizedLambdaExpression(EventType);

// 
[Benchmark]
public INotification CreateViaCompiledParameterizedExpression() =>
    CompiledParameterizedExpression.Invoke(_somethingCreatedEvent);

[Benchmark]
public INotification CreateViaCompiledParameterizedExpressionWithDynamicInvoke() =>
    CompiledParameterizedExpression.DynamicInvoke(_somethingCreatedEvent) as INotification;

и результат

BenchmarkDotNet=v0.12.1, OS=macOS 12.6.2 (21G320) [Darwin 21.6.0]
Intel Core i5-8259U CPU 2.30GHz (Coffee Lake), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=6.0.402
  [Host]     : .NET Core 6.0.10 (CoreCLR 6.0.1022.47605, CoreFX 6.0.1022.47605), X64 RyuJIT
  DefaultJob : .NET Core 6.0.10 (CoreCLR 6.0.1022.47605, CoreFX 6.0.1022.47605), X64 RyuJIT
Method Mean Error StdDev Ratio RatioSD
CreateViaCtor 7.314 ns 0.0606 ns 0.0537 ns 1.00 0.00
CreateViaExpression 482.540 ns 9.3683 ns 12.1814 ns 65.75 1.59
CreateViaCompiledParameterizedExpression 8.352 ns 0.1674 ns 0.1566 ns 1.14 0.02
CreateViaCompiledParameterizedExpressionWithDynamicInvoke 507.757 ns 2.4929 ns 2.2099 ns 69.43 0.57

Видно, что параметризованное выражение, вызываемое через Invoke, практически не уступает созданию через конструктор.

Но если вызывать это выражение через DynamicInvoke, то снова всё превращается в тыкву.