如何提高C#对象映射代码的性能

7
如何在保持公共接口的情况下进一步提高以下代码的性能:
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 });
}

基准测试代码

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


我已经更新了问题,并附上了我进行基准测试的详细信息。我希望尽可能接近基线甚至超过它。 - Muhammad Rehan Saeed
7
我发现的一个性能瓶颈是在使用new T()来定义类型参数时。我发现传递一个Func<T>()可以产生巨大的差异。如果你可以把你的代码编辑成一个 [mcve],这样我们就能轻松地运行测试,看看这种方法的差异有多大。(当然,它仍然无法达到你的基线...) - Jon Skeet
我已经更新了问题,并提供了一个链接到我的GitHub存储库,其中包含完整的解决方案文件,您可以打开并运行benchmark.NET项目,再也没有比这更容易的了。 - Muhammad Rehan Saeed
与我之前写的相反,顶部代码片段确实可以编译。仍然要记住:如果您想要能够使用值类型(结构)TDestination执行映射,则您的IMapper<TSource, TDestination>.Map方法必须返回TDestination - Kirill Shlenskiy
很酷。此时我会尝试Jon Skeet的Func<T>建议,尝试使用destinationCollection[i] = translator.Map(sourceItem)而不是使用Add,最后尝试内联(或者说消除)你的Map扩展方法(可能会或可能不会有所改变,因为方法体已经足够小了,编译器可能已经自动进行了内联 - 在发布后编译后查看你的MapList<TSource, TDestination>IL确认)。除了这3件事之外,我真的没有看到任何进一步的优化空间。 - Kirill Shlenskiy
显示剩余8条评论
1个回答

7

在评论中讨论并实施建议后,这是我能够想出的最有效的MapList<TSource, TDestination>实现:

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<TDestination>委托,从而保护了您的API。
当然,编译工厂委托的成本是不可忽略的,但在常见的映射场景中,我认为这些成本是值得的。

这真的很聪明。将Func<T> CreateNew()方法添加到IMapper接口中并按照@JonSkeet的建议删除new(),可以使基准速度提高1.21倍,而您的代码则慢了1.34倍,非常接近。我知道我说过不要更改接口,但我还不能确定该选择哪个。 - Muhammad Rehan Saeed

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接