- A+
性能测试结论:使用
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
结果对比
通过使用 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
来查看程序执行情况,如下图所示:
从图中我们可以看到,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 的性能提升了不少。
但是,由于本身性能基数比较大,所有依然存在性能问题