放弃 AutoMapper ,拥抱 Mapster

  • A+
所属分类:.NET技术
摘要

性能测试结论:使用 new {} 的方式性能最佳,其次是 Mapster ,最后是 AutoMapper最近在对一个业务接口做代码重构时,发现在使用 AutoMapper 做对象转换时,接口响应速度极慢,100多条数据,请求时长都超过了8秒。为了找出原因所在,我尝试将 EF Core 的相关查询和 实体转换拆分开来做分析,最终发现是由于使用 AutoMapper 时,性能出现了瓶颈。于是我尝试使用 select new {} 的 Linq 方式来硬编码转换,发现效率提升了几倍,基本 2秒内就能完成。出于好奇,我尝试对 AutoMapper 做一下性能对比测试。


性能测试结论:使用 new {} 的方式性能最佳,其次是 Mapster ,最后是 AutoMapper

最近在对一个业务接口做代码重构时,发现在使用 AutoMapper 做对象转换时,接口响应速度极慢,100多条数据,请求时长都超过了8秒。为了找出原因所在,我尝试将 EF Core 的相关查询和 实体转换拆分开来做分析,最终发现是由于使用 AutoMapper 时,性能出现了瓶颈。于是我尝试使用 select new {} 的 Linq 方式来硬编码转换,发现效率提升了几倍,基本 2秒内就能完成。出于好奇,我尝试对 AutoMapper 做一下性能对比测试。

测试步骤

测试环境

  • OS:Windows 10.0.19042.1348 (20H2/October2020Update)
  • CPU:Intel Core i5-7500 CPU 3.40GHz (Kaby Lake), 1 CPU, 4 logical and 4 physical cores
  • SDK:NET SDK=6.0.100
  • 压测工具:BenchmarkDotNet=v0.13.1

创建项目

dotnet new console -o ConsoleApp1 

安装依赖包

dotnet add package BenchmarkDotNet --version 0.13.1 dotnet add package AutoMapper --version 10.1.1 dotnet add package Mapster --version 7.2.0 

定义用于测试 Entity 和 DTO

public enum MyEnum {     [Description("进行中")]     Doing,     [Description("完成")]     Done } public class Entity {     public int Id { get; set; }     public Guid Oid { get; set; }     public string? NickName { get; set; }     public bool Created { get; set; }     public MyEnum State { get; set; } }  public class EntityDto {     public int Id { get; set; }     public Guid Oid { get; set; }     public string? NickName { get; set; }     public bool Created { get; set; }     public MyEnum Enum { get; set; }     public string? EnumString { get; set; } } 

配置 Entity 和 DTO 之间的转换关系

AutoMapper 配置

  public class AutoMapperProfile : Profile   {       public AutoMapperProfile()       {           CreateMap<Entity, EntityDto>()                .ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id))                .ForMember(dest => dest.Oid, opt => opt.MapFrom(src => src.Oid))                .ForMember(dest => dest.NickName, opt => opt.MapFrom(src => src.NickName))                .ForMember(dest => dest.Created, opt => opt.MapFrom(src => src.Created))                .ForMember(dest => dest.Enum, opt => opt.MapFrom(src => src.State))                .ForMember(dest => dest.EnumString, opt => opt.MapFrom(src => src.State.GetDescription()));       }   } 

Mapster 配置

public class MapsterProfile : TypeAdapterConfig {     public MapsterProfile()     {         ForType<Entity, EntityDto>()              .Map(dest => dest.Id, src => src.Id)              .Map(dest => dest.Oid, src => src.Oid)              .Map(dest => dest.NickName, src => src.NickName)              .Map(dest => dest.Created, src => src.Created)              .Map(dest => dest.Enum, src => src.State)              .Map(dest => dest.EnumString, src => src.State.GetDescription());     } } 

创建性能测试类

public class PerformanceTest {     private IReadOnlyList<entity> _entities;     private readonly AutoMapper.IMapper _autoMapper;     private readonly Mapper _mapsterMapper;      public PerformanceTest()     {         var mocker = new AutoMocker();         _entities = Enumerable.Range(0, 1000000).Select(x => mocker.CreateInstance<entity>()).ToList();          var configuration = new AutoMapper.MapperConfiguration(cfg => cfg.AddProfile<AutoMapperProfile>());          _autoMapper = configuration.CreateMapper();          _mapsterMapper = new MapsterMapper.Mapper(new MapsterProfile());     }      [Benchmark]     public void Constructor()     {         var dtos = _entities.Select(x => new EntityDto         {             Id = x.Id,             Oid = x.Oid,             NickName = x.NickName,             Created = x.Created,             Enum = x.State,             EnumString = x.State.GetDescription(),         });     }      [Benchmark]     public void AutoMapper()     {         var dtos = _autoMapper.Map<ienumerable<entitydto>>(_entities);      }     [Benchmark]     public void Mapster()     {         var dtos = _mapsterMapper.Map<ienumerable<entitydto>>(_entities);     } } 

执行性能测试

var summary = BenchmarkRunner.Run<PerformanceTest>(); 
dotnet run --project .ConsoleApp1.csproj -c Release 

结果对比

放弃 AutoMapper ,拥抱 Mapster

通过使用 BenchmarkDotNet 来进行压测对比。从上图我们可以看出,使用 Constructor(直接创建对象) 的方式性能是最高的,然后就是 Mapster,最后才是 AutoMapper

使用 ReadableExpressions.Visualizers 查看 Execution Plan

在项目中一直在使用 AutoMapper 来做对象转换,看 Github 活跃度,按理说不应该出现这么明显的性能问题。好奇心驱使我项研究一下,通过和作者沟通后了解到,'AutoMapper' 本身会有一个所谓的执行计划 execution plan,可以通过安装插件 ReadableExpressions.Visualizers 来查看。

在 AutoMapper 的配置地方添加如下代码:

var executionPlan = configuration.BuildExecutionPlan(typeof(Entity), typeof(EntityDto)); var executionPlanStr = executionPlan.ToReadableString(); 

查看 executionPlanStr 值,如下所示:

(src, dest, ctxt) => {     EntityDto typeMapDestination;     return (src == null)         ? null         : {             typeMapDestination = dest ?? new EntityDto();             try             {                 var resolvedValue =                 {                     try                     {                         Entity src;                         return (((src = src) == null) || false) ? default(int) : src.Id;                     }                     catch (NullReferenceException)                     {                         return default(int);                     }                     catch (ArgumentNullException)                     {                         return default(int);                     }                 };                  typeMapDestination.Id = resolvedValue;             }             catch (Exception ex)             {                 return throw new AutoMapperMappingException(                     "Error mapping types.",                     ex,                     AutoMapper.TypePair,                     TypeMap,                     PropertyMap);             }             try             {                 var resolvedValue =                 {                     try                     {                         Entity src;                         return (((src = src) == null) || false) ? default(Guid) : src.Oid;                     }                     catch (NullReferenceException)                     {                         return default(Guid);                     }                     catch (ArgumentNullException)                     {                         return default(Guid);                     }                 };                  typeMapDestination.Oid = resolvedValue;             }             catch (Exception ex)             {                 return throw new AutoMapperMappingException(                     "Error mapping types.",                     ex,                     AutoMapper.TypePair,                     TypeMap,                     PropertyMap);             }             try             {                 var resolvedValue =                 {                     try                     {                         Entity src;                         return (((src = src) == null) || false) ? null : src.NickName;                     }                     catch (NullReferenceException)                     {                         return null;                     }                     catch (ArgumentNullException)                     {                         return null;                     }                 };                  var propertyValue = (resolvedValue == null) ? null : resolvedValue;                 typeMapDestination.NickName = propertyValue;             }             catch (Exception ex)             {                 return throw new AutoMapperMappingException(                     "Error mapping types.",                     ex,                     AutoMapper.TypePair,                     TypeMap,                     PropertyMap);             }             try             {                 var resolvedValue =                 {                     try                     {                         Entity src;                         return (((src = src) == null) || false) ? default(bool) : src.Created;                     }                     catch (NullReferenceException)                     {                         return default(bool);                     }                     catch (ArgumentNullException)                     {                         return default(bool);                     }                 };                  typeMapDestination.Created = resolvedValue;             }             catch (Exception ex)             {                 return throw new AutoMapperMappingException(                     "Error mapping types.",                     ex,                     AutoMapper.TypePair,                     TypeMap,                     PropertyMap);             }             try             {                 var resolvedValue =                 {                     try                     {                         Entity src;                         return (((src = src) == null) || false) ? default(MyEnum) : src.State;                     }                     catch (NullReferenceException)                     {                         return default(MyEnum);                     }                     catch (ArgumentNullException)                     {                         return default(MyEnum);                     }                 };                  typeMapDestination.Enum = resolvedValue;             }             catch (Exception ex)             {                 return throw new AutoMapperMappingException(                     "Error mapping types.",                     ex,                     AutoMapper.TypePair,                     TypeMap,                     PropertyMap);             }             try             {                 var resolvedValue =                 {                     try                     {                         return ((Enum)src.State).GetDescription();                     }                     catch (NullReferenceException)                     {                         return null;                     }                     catch (ArgumentNullException)                     {                         return null;                     }                 };                  var propertyValue = (resolvedValue == null) ? null : resolvedValue;                 typeMapDestination.EnumString = propertyValue;             }             catch (Exception ex)             {                 return throw new AutoMapperMappingException(                     "Error mapping types.",                     ex,                     AutoMapper.TypePair,                     TypeMap,                     PropertyMap);             }              return typeMapDestination;         }; } 

使用 dotTrace 进行性能跟踪

通过使用 JetBrains dotTrace 来查看程序执行情况,如下图所示:

放弃 AutoMapper ,拥抱 Mapster

从图中我们可以看到,GetDescription 方法性能占用高达 8.38%。我尝试将这个枚举转字符串的扩展方法每次都返回固定值,如下所示:

public static class Extension {     public static string GetDescription(this Enum value)     {         return "aaa";         //var field = value.GetType().GetField(value.ToString());         //if (field == null) return "";         //return Attribute.GetCustomAttribute(field,         //    typeof(DescriptionAttribute)) is not DescriptionAttribute attribute         //    ? value.ToString()         //    : attribute.Description;     } }  

再次进行性能测试,发现 AutoMapper 的性能提升了不少。

放弃 AutoMapper ,拥抱 Mapster

但是,由于本身性能基数比较大,所有依然存在性能问题

相关参考