性能測試結論:使用
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 的性能提升了不少。
但是,由於本身性能基數比較大,所有依然存在性能問題