В одном из проектов сложилась ситуация, когда есть интерфейс 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, то снова всё превращается в тыкву.