【问题标题】:How to Improve Performance of C# Object Mapping Code如何提高 C# 对象映射代码的性能
【发布时间】:2017-09-30 07:36:46
【问题描述】:

如何在保持公共接口的同时进一步提高以下代码的性能:

public interface IMapper<in TSource, in TDestination>
{
    void Map(TSource source, TDestination destination);
}

public static TDestination Map<TSource, TDestination>(
    this IMapper<TSource, TDestination> translator,
    TSource source)
    where TDestination : new()
{
    var destination = new TDestination();
    translator.Map(source, destination);
    return destination;
}

public static List<TDestination> MapList<TSource, TDestination>(
    this IMapper<TSource, TDestination> translator,
    List<TSource> source)
    where TDestination : new()
{
    var destinationCollection = new List<TDestination>(source.Count);
    foreach (var sourceItem in source)
    {
        var destinationItem = translator.Map(sourceItem);
        destinationCollection.Add(destinationItem);
    }
    return destinationCollection;
}

示例用法

public class MapFrom { public string Property { get; set; } }

public class MapTo { public string Property { get; set; } }

public class Mapper : IMapper<MapFrom, MapTo>
{
    public void Map(MapFrom source, MapTo destination)
    {
        destination.Property = source.Property;
    }
}

var mapper = new Mapper();
var mapTo = mapper.Map(new MapFrom() { Property = "Foo" });
var mapToList = mapper.MapList(
    new List<MapFrom>() 
    {
        new MapFrom() { Property = "Foo" } 
    });

当前基准

当我针对原始手动转换运行基准测试时,我得到以下数字:

|             Method |  Job | Runtime |      Mean |     Error |    StdDev |       Min |       Max | Scaled | ScaledSD |  Gen 0 | Allocated |
|------------------- |----- |-------- |----------:|----------:|----------:|----------:|----------:|-------:|---------:|-------:|----------:|
|           Baseline |  Clr |     Clr |  1.969 us | 0.0354 us | 0.0332 us |  1.927 us |  2.027 us |   1.00 |     0.00 | 2.0523 |   6.31 KB |
|  Mapper            |  Clr |     Clr |  9.016 us | 0.1753 us | 0.2019 us |  8.545 us |  9.419 us |   4.58 |     0.12 | 2.0447 |   6.31 KB |
|           Baseline | Core |    Core |  1.820 us | 0.0346 us | 0.0355 us |  1.777 us |  1.902 us |   1.00 |     0.00 | 2.0542 |   6.31 KB |
|  Mapper            | Core |    Core |  9.043 us | 0.1725 us | 0.1613 us |  8.764 us |  9.294 us |   4.97 |     0.13 | 2.0447 |   6.31 KB |

这是基线的代码:

var mapTo = new MapTo() { Property = mapFrom.Property };
var mapToCollection = new List<MapTo>(this.mapFrom.Count);
foreach (var item in this.mapFrom)
{
    destination.Add(new MapTo() { Property = item.Property });
}

基准代码

我在Dotnet-Boxed/Framework GitHub 存储库中有一个完整的项目,其中包含映射器和 Benchmark.NET 项目。

【问题讨论】:

  • 我已经用我所做的基准测试中的详细信息更新了这个问题。我希望尽可能接近基线,甚至可能超过它。
  • 为什么不实现 ISharedProperties 接口并使用 O(1) 赋值?
  • @shadow32 MapFromMapTo 在我的示例中碰巧具有相同的属性名称,但情况并非总是如此。想想 Automapper。
  • “甚至可能超过它” - 是的,这不会发生,除非你的基线不是真正的基线。您的MapList 方法非常简单。如果你想拥有虚拟方法,你将不得不在虚拟调度上花费一些 CPU 时间。另外,是否有理由重新发明轮子?为什么不简单地使用 Automapper? AFAIK,Automapper 使用 Reflection.Emit 生成映射代码并编译它,以避免属性名称查找。
  • 我发现的一个性能瓶颈是将new T() 用于受约束的类型参数。我发现传入Func&lt;T&gt;() 会产生巨大的影响。如果您可以将您的代码编辑成minimal reproducible example,让我们真正轻松地运行测试,我可以看到它有多大的不同。 (但它仍然不会达到你的基线,当然......)

标签: c# .net performance allocation unchecked


【解决方案1】:

在实施了 cmets 中讨论的建议之后,这是我能想到的最有效的 MapList&lt;TSource, TDestination&gt; 实施:

using System;
using System.Collections.Generic;
using System.Linq.Expressions;

public static List<TDestination> MapList<TSource, TDestination>(
    this IMapper<TSource, TDestination> translator,
    List<TSource> source)
    where TDestination : new()
{
    var destinationCollection = new List<TDestination>(source.Count);

    foreach (var sourceItem in source)
    {
        TDestination dest = Factory<TDestination>.Instance();
        translator.Map(sourceItem, dest);
        destinationCollection.Add(dest);
    }

    return destinationCollection;
}

static class Factory<T>
{
    // Cached "return new T()" delegate.
    internal static readonly Func<T> Instance = CreateFactory();

    private static Func<T> CreateFactory()
    {
        NewExpression newExpr = Expression.New(typeof(T));

        return Expression
            .Lambda<Func<T>>(newExpr)
            .Compile();
    }
}

请注意,我设法利用了 Jon Skeet 的建议使用 new TDestination() 而无需调用者提供 Func&lt;TDestination&gt; 委托,从而保留了您的 API。

当然,编译工厂委托的成本是不可忽略的,但在常见的映射场景中,我认为这是值得的。

【讨论】:

  • 这真是太聪明了。向IMapper 接口添加Func&lt;T&gt; CreateNew() 方法并按照@JonSkeet 的建议删除new() 是基线速度的1.21 倍,而您的代码慢了1.34 倍,非常接近。我知道我说过不要更改界面,但我无法决定使用哪个界面。
猜你喜欢
  • 2010-10-07
  • 1970-01-01
  • 2023-02-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多