📜 ⬆️ ⬇️

We write your mapper for .NET Standard 2.0

In today's article, I would like to tell you about a short adventure in writing my mapper for .NET Standard 2.0. The link to github and the results of the benchmarks are attached.


I think it’s no secret to any of you what a mapper is and what it is for. Literally at every step in the process of work, we encounter some examples of mappings (or transformations) of data from one type to another. These include mapping of records from the repository to the domain model, mapping the response of the remote service to the view model and only then to the domain model, etc. Often, on the border of the level of abstraction, there are input and output data formats and it is precisely at the moments of interaction of abstractions that such a thing as a mapper can show itself in all its glory, bringing with it a substantial saving of time and effort for the developer and, as a result, taking on share of total system performance.


Based on this, MVP requirements can also be described:


  1. Speed ​​performance (less performance & memory impact);
  2. Ease of use (clean & easy to use API).

As for the first point, BenchmarkDotNet and thoughtful implementation, not without some optimizations, will help us with this. For the second, I wrote a simple unit test, which, in some way, acts as the documentation for the API of our mapper:


[TestMethod] public void WhenMappingExist_Then_Map() { var dto = new CustomerDto { Id = 42, Title = "Test", CreatedAtUtc = new DateTime(2017, 9, 3), IsDeleted = true }; mapper.Register<CustomerDto, Customer>(); var customer = mapper.Map<CustomerDto, Customer>(dto); Assert.AreEqual(42, customer.Id); Assert.AreEqual("Test", customer.Title); Assert.AreEqual(new DateTime(2017, 9, 3), customer.CreatedAtUtc); Assert.AreEqual(true, customer.IsDeleted); } 

So, we need to implement only 2 simple methods:


  1. void Register<TSource, TDest>() ;
  2. TDest Map<TSource, TDest>(TSource source) .

check in


In fact, the registration process can be carried out when you first call the Map method, thus becoming redundant. However, I rendered it separately for the following reasons:


  1. For verification, in the absence of a default constructor (or the inability to perform mapping of the resulting type) in my opinion, this should be reported as early as possible at the configuration stage, thereby observing the Fail fast principle. Otherwise, the error of the impossibility of creating an instance of a type may overtake us already at the stage of the execution of the infrastructure code or business logic;
  2. For expansion, the API is currently extremely simple and under the hood implies mapping based on naming conventions, however, it is likely that very soon we will want to introduce rules for mapping certain fields, the value for assignment of which may be the result of the method . In this case, in order to also observe the principle of Single Responsible, such a division seems to me quite natural.

If the Map method in any mapper is the main one and the lion's share of the execution time falls on it, then the Register method is the opposite, for each pair of types it is called only once at the configuration stage. That is why it is an excellent candidate for accomplishing all the necessary "heavy" manipulations: generating an optimal mapping execution plan and, as a result, further caching the obtained results.


Thus, its implementation should include:


  1. Building a plan for creating and initializing an instance of the required type;
  2. Caching results.

Execution plan


In C #, there are not so many ways available to us to create and initialize an instance of a type at runtime, and the higher the level of abstraction of a particular method, the less optimal from the point of view of execution time. Earlier, I came across a similar choice in my other small project called FsContainer and therefore the following results were not surprising to me.


 BenchmarkDotNet=v0.10.9, OS=Windows 8.1 (6.3.9600) Processor=Intel Core i5-5200U CPU 2.20GHz (Broadwell), ProcessorCount=4 Frequency=2143473 Hz, Resolution=466.5326 ns, Timer=TSC .NET Core SDK=2.0.0 [Host] : .NET Core 2.0.0 (Framework 4.6.00001.0), 64bit RyuJIT DefaultJob : .NET Core 2.0.0 (Framework 4.6.00001.0), 64bit RyuJIT 

  | Method | Mean | Error | StdDev | Median | |---------------------------- |-----------:|----------:|----------:|-----------:| | ExpressionCtorObjectBuilder | 8.548 ns | 0.2764 ns | 0.4541 ns | 8.608 ns | | ActivatorCreateInstance | 79.379 ns | 1.6812 ns | 3.1987 ns | 78.890 ns | | ConstructorInfoInvoke | 164.445 ns | 3.3355 ns | 4.3371 ns | 164.016 ns | | DynamicMethodILGenerator | 5.859 ns | 0.2455 ns | 0.3015 ns | 5.819 ns | | NewCtor | 6.989 ns | 0.2615 ns | 0.5741 ns | 6.756 ns | 

Although ConstructorInfo.Invoke and Activator.CreateInstance fairly easy to use, in this list, by a wide margin, they are obvious outsiders due to the fact that they use RuntimeType and System.Reflection in the details of their implementations. This is quite acceptable in everyday tasks, but it is completely inappropriate within our requirements, where creating an instance of the type is the narrowest bottle neck in terms of performance.


With regard to the use of Expression and DynamicMethod , here without surprises, the result of execution are pointers to compiled functions that can only be called by passing the corresponding arguments.


Although the Delegate compiled by generating the IL code on the fly works slightly faster, it does not include the code for initializing the type instance. Moreover, for me personally, the reproduction of IL instructions via ilgen.Emit is a very nontrivial ilgen.Emit .


 var dynamicMethod = new DynamicMethod("Create_" + ctorInfo.Name, ctorInfo.DeclaringType, new[] { typeof(object[]) }); var ilgen = dynamicMethod.GetILGenerator(); ilgen.Emit(OpCodes.Newobj, ctorInfo); ilgen.Emit(OpCodes.Ret); return dynamicMethod.CreateDelegate(typeof(Func<TDest>)); 

That is why I stopped at the implementation using Expression :


 var body = Expression.MemberInit( Expression.New(typeof(TDest)), props ); return Expression.Lambda<Func<TSource, TDest>>(body, orig).Compile(); 

Caching


To cache the compiled delegate, which will later be used to perform the mapping, I chose between Dictionary and Hashtable . Looking ahead, I would like to note that the key role is played not only by the type of collection, but also by the type of key by which the selection will be carried out. To check this statement, a separate benchmark was written and the following results were obtained:


 BenchmarkDotNet=v0.10.9, OS=Windows 8.1 (6.3.9600) Processor=Intel Core i5-5200U CPU 2.20GHz (Broadwell), ProcessorCount=4 Frequency=2143473 Hz, Resolution=466.5326 ns, Timer=TSC .NET Core SDK=2.0.0 [Host] : .NET Core 2.0.0 (Framework 4.6.00001.0), 64bit RyuJIT DefaultJob : .NET Core 2.0.0 (Framework 4.6.00001.0), 64bit RyuJIT 

  | Method | Mean | Error | StdDev | |-------------------- |----------:|----------:|----------:| | DictionaryTuple | 80.37 ns | 1.6473 ns | 1.6179 ns | | DictionaryTypeTuple | 49.35 ns | 0.6235 ns | 0.5832 ns | | HashtableTuple | 103.07 ns | 2.6081 ns | 2.4397 ns | | HashtableTypeTuple | 71.51 ns | 0.8679 ns | 0.7694 ns | 

Taking this into account, the following conclusions can be made:


  1. The use of the type Dictionary preferable to Hashtable in terms of the time spent on getting the collection item
  2. Use as a TypeTuple ( src ) type key is preferable to Tuple<Type, Type> in terms of time spent on Equals & GetHashCode ;

Mapping


The internal implementation of the Map method should be extremely simple and optimized due to the fact that this method will be called in 99.9% of cases. Therefore, all we need to do is to quickly find the link to the previously compiled Delegate in the cache and return the result of its execution:


 public TDest Map<TSource, TDest>(TSource source) { var key = new TypeTuple(typeof(TSource), typeof(TDest)); var activator = GetMap(key); return ((Func<TSource, TDest>)activator)(source); } 

results


As a result, I would like to cite the results of the final measurements of the existing (and currently up to date) mappers:


 BenchmarkDotNet=v0.10.9, OS=Windows 8.1 (6.3.9600) Processor=Intel Core i5-5200U CPU 2.20GHz (Broadwell), ProcessorCount=4 Frequency=2143473 Hz, Resolution=466.5326 ns, Timer=TSC .NET Core SDK=2.0.0 [Host] : .NET Core 2.0.0 (Framework 4.6.00001.0), 64bit RyuJIT DefaultJob : .NET Core 2.0.0 (Framework 4.6.00001.0), 64bit RyuJIT 

  | Method | Mean | Error | StdDev | |----------------------- |-----------:|----------:|----------:| | FsMapperBenchmark | 84.492 ns | 1.6972 ns | 1.6669 ns | | ExpressMapperBenchmark | 251.161 ns | 4.6736 ns | 4.3717 ns | | AutoMapperBenchmark | 204.142 ns | 4.2002 ns | 9.1309 ns | | MapsterBenchmark | 90.949 ns | 1.6393 ns | 1.4532 ns | | AgileMapperBenchmark | 218.021 ns | 3.0921 ns | 2.7410 ns | | CtorMapperBenchmark | 7.806 ns | 0.2472 ns | 0.2312 ns | 

The source code of the project is available on github: https://github.com/FSou1/FsMapper .


Thank you for reading to the end and, I hope, this article was useful to you. Write in the comments that in your opinion could still be optimized.


')

Source: https://habr.com/ru/post/341502/


All Articles