PocoEmit遥遥领先于AutoMapper之循环引用

一、什么是循环引用

循环引用就是类型相互依赖

1. 比如A类有B类的属性,B类也有A类的属性

  • 这有什么问题呢?
  • 编写生成A的代码需要遍历A的所有属性
  • 构造B类型属性是A代码的一部分,B代码又含有A类型属性
  • 这就是一个编译死循环

2. 其他循环引用的例子

  • 链表结构只有一个类型也是类型循环引用
  • A-B-C-A等更长的引用链条也会构成类型循环引用

二、举个树状结构的Case

  • 树状结构在实际应用中很常见

1. 导航菜单代码

导航菜单是一个典型的树状结构

public class Menu {     public int Id { get; set; }     public string Name { get; set; }     public string Description { get; set; }     public List<Menu> Children { get; set; }     public static Menu GetMenu()     {         var programs = new Menu { Id = 2, Name = "Programs", Description = "程序" };         var documents = new Menu { Id = 3, Name = "Documents", Description = "文档" };         var settings = new Menu { Id = 4, Name = "Settings", Description = "设置" };         var help = new Menu { Id = 5, Name = "Help", Description = "帮助" };         var run = new Menu { Id = 6, Name = "Run", Description = "运行" };         var shutdown = new Menu { Id = 7, Name = "Shut Down", Description = "关闭" };         var start = new Menu { Id = 1, Name = "Start", Description = "开始", Children = [programs, documents, settings, help, run, shutdown] };         return start;     } } 

2. 把Menu转化为MenuDTO

2.1 PocoEmit执行代码

  • 代码中多加了UseCollection
  • 如果全局开启了集合就不需要这行代码
var menu = Menu.GetMenu(); var mapper = PocoEmit.Mapper.Create()     .UseCollection(); var dto = mapper.Convert<Menu, MenuDTO>(menu); 

2.2 执行效果如下:

{   "$id": "1",   "Id": 1,   "Name": "Start",   "Description": "u5F00u59CB",   "Children": {     "$id": "2",     "$values": [       {         "$id": "3",         "Id": 2,         "Name": "Programs",         "Description": "u7A0Bu5E8F",         "Children": null       },       {         "$id": "4",         "Id": 3,         "Name": "Documents",         "Description": "u6587u6863",         "Children": null       },       {         "$id": "5",         "Id": 4,         "Name": "Settings",         "Description": "u8BBEu7F6E",         "Children": null       },       {         "$id": "6",         "Id": 5,         "Name": "Help",         "Description": "u5E2Eu52A9",         "Children": null       },       {         "$id": "7",         "Id": 6,         "Name": "Run",         "Description": "u8FD0u884C",         "Children": null       },       {         "$id": "8",         "Id": 7,         "Name": "Shut Down",         "Description": "u5173u95ED",         "Children": null       }     ]   } } 

3. 与AutoMapper性能对比如下

Method Mean Error StdDev Median Ratio RatioSD Gen0 Gen1 Allocated Alloc Ratio
Auto 320.14 ns 0.420 ns 0.484 ns 320.10 ns 5.51 0.10 0.0751 0.0003 1296 B 2.95
AutoFunc 289.80 ns 6.580 ns 7.313 ns 295.77 ns 4.98 0.15 0.0751 0.0003 1296 B 2.95
Poco 58.17 ns 1.031 ns 1.103 ns 58.17 ns 1.00 0.03 0.0255 - 440 B 1.00
PocoFunc 48.10 ns 1.059 ns 1.087 ns 49.06 ns 0.83 0.02 0.0255 - 440 B 1.00
  • AutoMapper耗时是Poco的5倍多
  • AutoMapper内存是Poco的近3倍
  • 哪怕是用上AutoMapper内部生成的委托也挽救不了多少局面

4. 我们增加无循环引用再测试一下

4.1 无循环引用菜单

public class Menu0 {     public int ParentId { get; set; }     public int Id { get; set; }     public string Name { get; set; }     public string Description { get; set; }      public static List<Menu0> GetMenus()     {         var start = new Menu0 { Id = 1, Name = "Start", Description = "开始", ParentId = 0 };         var programs = new Menu0 { Id = 2, Name = "Programs", Description = "程序", ParentId = 1 };         var documents = new Menu0 { Id = 3, Name = "Documents", Description = "文档", ParentId = 1 };         var settings = new Menu0 { Id = 4, Name = "Settings", Description = "设置", ParentId = 1 };         var help = new Menu0 { Id = 5, Name = "Help", Description = "帮助", ParentId = 1 };         var run = new Menu0 { Id = 6, Name = "Run", Description = "运行" , ParentId = 1 };         var shutdown = new Menu0 { Id = 7, Name = "Shut Down", Description = "关闭", ParentId = 1 };         return [start, programs, documents, settings, help, run, shutdown];     } } 

4.2 性能测试如下

Method Mean Error StdDev Median Ratio RatioSD Gen0 Gen1 Allocated Alloc Ratio
Auto 320.14 ns 0.420 ns 0.484 ns 320.10 ns 5.51 0.10 0.0751 0.0003 1296 B 2.95
Auto0 110.60 ns 1.130 ns 1.302 ns 110.30 ns 1.90 0.04 0.0264 - 456 B 1.04
AutoFunc 289.80 ns 6.580 ns 7.313 ns 295.77 ns 4.98 0.15 0.0751 0.0003 1296 B 2.95
Poco 58.17 ns 1.031 ns 1.103 ns 58.17 ns 1.00 0.03 0.0255 - 440 B 1.00
Poco0 60.80 ns 0.176 ns 0.202 ns 60.73 ns 1.05 0.02 0.0227 - 392 B 0.89
PocoFunc 48.10 ns 1.059 ns 1.087 ns 49.06 ns 0.83 0.02 0.0255 - 440 B 1.00
  • Auto0是AutoMapper把Menu0列表转化为DTO的case
  • Poco0是Poco把Menu0列表转化为DTO的case
  • AutoMapper循环引用处理耗时和内存都是列表的3倍
  • Poco循环引用处理和列表性能差不多
  • 当然就算是无循环引用的列表处理,AutoMapper耗时也几乎是Poco的两倍
  • 这充分说明AutoMapper处理循环引用是有问题的

5. 先对比一下AutoMapper有无循环引用的代码

5.1 AutoMapper无循环引用的代码如下

T __f<T>(System.Func<T> f) => f(); (Func<List<Menu0>, List<Menu0DTO>, ResolutionContext, List<Menu0DTO>>)((     List<Menu0> source,      List<Menu0DTO> mapperDestination,      ResolutionContext context) => //List<Menu0DTO>     (source == null) ?          new List<Menu0DTO>() :          __f(() => {             try             {                 List<Menu0DTO> collectionDestination = null;                 List<Menu0DTO> passedDestination = null;                 passedDestination = mapperDestination;                 collectionDestination = passedDestination ?? new List<Menu0DTO>();                 collectionDestination.Clear();                 List<Menu0>.Enumerator enumerator = default;                 Menu0 item = null;                 enumerator = source.GetEnumerator();                 try                 {                     while (true)                     {                         if (enumerator.MoveNext())                         {                             item = enumerator.Current;                             collectionDestination.Add(((Func<Menu0, Menu0DTO, ResolutionContext, Menu0DTO>)((                                 Menu0 source_1,                                  Menu0DTO destination,                                  ResolutionContext context) => //Menu0DTO                                 (source_1 == null) ?                                      (destination == null) ? (Menu0DTO)null : destination :                                      __f(() => {                                         Menu0DTO typeMapDestination = null;                                         typeMapDestination = destination ?? new Menu0DTO();                                         try                                         {                                             typeMapDestination.ParentId = source_1.ParentId;                                         }                                         catch (Exception ex)                                         {                                             throw TypeMapPlanBuilder.MemberMappingError(                                                 ex,                                                 default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);                                         }                                         try                                         {                                             typeMapDestination.Id = source_1.Id;                                         }                                         catch (Exception ex)                                         {                                             throw TypeMapPlanBuilder.MemberMappingError(                                                 ex,                                                 default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);                                         }                                         try                                         {                                             typeMapDestination.Name = source_1.Name;                                         }                                         catch (Exception ex)                                         {                                             throw TypeMapPlanBuilder.MemberMappingError(                                                 ex,                                                 default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);                                         }                                         try                                         {                                             typeMapDestination.Description = source_1.Description;                                         }                                         catch (Exception ex)                                         {                                             throw TypeMapPlanBuilder.MemberMappingError(                                                 ex,                                                 default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);                                         }                                         return typeMapDestination;                                     })))                             .Invoke(                                 item,                                 (Menu0DTO)null,                                 context));                         }                         else                         {                             goto LoopBreak;                         }                     }                     LoopBreak:;                 }                 finally                 {                     enumerator.Dispose();                 }                 return collectionDestination;             }             catch (Exception ex)             {                 throw MapperConfiguration.GetMappingError(                     ex,                     default(MapRequest)/*NOTE: Provide the non-default value for the Constant!*/);             }         })); 

5.2 AutoMapper循环引用的代码如下

5.2.1 Menu转MenuDTO
T __f<T>(System.Func<T> f) => f(); (Func<Menu, MenuDTO, ResolutionContext, MenuDTO>)((     Menu source,      MenuDTO destination,      ResolutionContext context) => //MenuDTO     (source == null) ?          (destination == null) ? (MenuDTO)null : destination :          __f(() => {             MenuDTO typeMapDestination = null;             ResolutionContext.CheckContext(ref context);             return ((MenuDTO)context.GetDestination(                 source,                 typeof(MenuDTO))) ??                  __f(() => {                     typeMapDestination = destination ?? new MenuDTO();                     context.CacheDestination(                         source,                         typeof(MenuDTO),                         typeMapDestination);                     typeMapDestination;                     try                     {                         typeMapDestination.Id = source.Id;                     }                     catch (Exception ex)                     {                         throw TypeMapPlanBuilder.MemberMappingError(                             ex,                             default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);                     }                     try                     {                         typeMapDestination.Name = source.Name;                     }                     catch (Exception ex)                     {                         throw TypeMapPlanBuilder.MemberMappingError(                             ex,                             default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);                     }                     try                     {                         typeMapDestination.Description = source.Description;                     }                     catch (Exception ex)                     {                         throw TypeMapPlanBuilder.MemberMappingError(                             ex,                             default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);                     }                     try                     {                         List<Menu> resolvedValue = null;                         List<MenuDTO> mappedValue = null;                         resolvedValue = source.Children;                         mappedValue = (resolvedValue == null) ?                              new List<MenuDTO>() :                              context.MapInternal<List<Menu>, List<MenuDTO>>(                                 resolvedValue,                                 (destination == null) ? (List<MenuDTO>)null :                                      typeMapDestination.Children,                                 (MemberMap)default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);                         typeMapDestination.Children = mappedValue;                     }                     catch (Exception ex)                     {                         throw TypeMapPlanBuilder.MemberMappingError(                             ex,                             default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);                     }                     return typeMapDestination;                 });         })); 
5.2.2 List<Menu>转List<MenuDTO>
T __f<T>(System.Func<T> f) => f(); (Func<List<Menu>, List<MenuDTO>, ResolutionContext, List<MenuDTO>>)((     List<Menu> source,     List<MenuDTO> mapperDestination,     ResolutionContext context) => //List<MenuDTO>     (source == null) ?         new List<MenuDTO>() :         __f(() => {             try             {                 List<MenuDTO> collectionDestination = null;                 List<MenuDTO> passedDestination = null;                 ResolutionContext.CheckContext(ref context);                 passedDestination = mapperDestination;                 collectionDestination = passedDestination ?? new List<MenuDTO>();                 collectionDestination.Clear();                 List<Menu>.Enumerator enumerator = default;                 Menu item = null;                 enumerator = source.GetEnumerator();                 try                 {                     while (true)                     {                         if (enumerator.MoveNext())                         {                             item = enumerator.Current;                             collectionDestination.Add(((Func<Menu, MenuDTO, ResolutionContext, MenuDTO>)((                                 Menu source_1,                                 MenuDTO destination,                                 ResolutionContext context) => //MenuDTO                                 (source_1 == null) ?                                     (destination == null) ? (MenuDTO)null : destination :                                     __f(() => {                                         MenuDTO typeMapDestination = null;                                         ResolutionContext.CheckContext(ref context);                                         return ((MenuDTO)context.GetDestination(                                             source_1,                                             typeof(MenuDTO))) ??                                             __f(() => {                                                 typeMapDestination = destination ?? new MenuDTO();                                                 context.CacheDestination(                                                     source_1,                                                     typeof(MenuDTO),                                                     typeMapDestination);                                                 typeMapDestination;                                                 try                                                 {                                                     typeMapDestination.Id = source_1.Id;                                                 }                                                 catch (Exception ex)                                                 {                                                     throw TypeMapPlanBuilder.MemberMappingError(                                                         ex,                                                         default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);                                                 }                                                 try                                                 {                                                     typeMapDestination.Name = source_1.Name;                                                 }                                                 catch (Exception ex)                                                 {                                                     throw TypeMapPlanBuilder.MemberMappingError(                                                         ex,                                                         default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);                                                 }                                                 try                                                 {                                                     typeMapDestination.Description = source_1.Description;                                                 }                                                 catch (Exception ex)                                                 {                                                     throw TypeMapPlanBuilder.MemberMappingError(                                                         ex,                                                         default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);                                                 }                                                 try                                                 {                                                     List<Menu> resolvedValue = null;                                                     List<MenuDTO> mappedValue = null;                                                     resolvedValue = source_1.Children;                                                     mappedValue = (resolvedValue == null) ?                                                         new List<MenuDTO>() :                                                         context.MapInternal<List<Menu>, List<MenuDTO>>(                                                             resolvedValue,                                                             (destination == null) ? (List<MenuDTO>)null :                                                                 typeMapDestination.Children,                                                             (MemberMap)default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);                                                     typeMapDestination.Children = mappedValue;                                                 }                                                 catch (Exception ex)                                                 {                                                     throw TypeMapPlanBuilder.MemberMappingError(                                                         ex,                                                         default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);                                                 }                                                 return typeMapDestination;                                             });                                     })))                             .Invoke(                                 item,                                 (MenuDTO)null,                                 context));                         }                         else                         {                             goto LoopBreak;                         }                     }                     LoopBreak:;                 }                 finally                 {                     enumerator.Dispose();                 }                 return collectionDestination;             }             catch (Exception ex)             {                 throw MapperConfiguration.GetMappingError(                     ex,                     default(MapRequest)/*NOTE: Provide the non-default value for the Constant!*/);             }         })); 

5.3 AutoMapper有无循环引用的代码分析如下

  • 循环引用的代码有2段,1段处理Menu,另1段处理List<Menu>
  • 直接对比处理List<Menu>部分
  • 很明显有循环引用部分多了不少特殊代码
5.3.1 AutoMapper循环引用多出以下代码
  • ResolutionContext.CheckContext消耗内存
  • context.GetDestination消耗内存和cpu
  • context.CacheDestination消耗内存和cpu
  • context.MapInternal用于调用代码
5.3.2 AutoMapper代码总结
  • MapInternal用于解决编译死循环的问题
  • GetDestination和CacheDestination用于解决执行死循环的问题
  • 但是这个case没有对象重复引用,没有执行死循环
  • 也就是说这里的GetDestination和CacheDestination只是消耗内存和cpu做无用功
  • 更让人无法接受的是,做这些无用功的消耗居然是正常代码的好几倍
  • 在无循环引用代码中ResolutionContext就是个摆设,无任何作用

6. 执行死循环该怎么处理呢

  • .net序列化给了我们答案
  • 序列化默认不支持对象循环引用,需要特殊配置,这是为了照顾大部分情况下的性能

6.1 序列化对象循环引用代码

Node node9 = new() { Id = 9, Name = "node9" }; Node node8 = new() { Id = 8, Name = "node8", Next = node9 }; Node node7 = new() { Id = 7, Name = "node7", Next = node8 }; Node node6 = new() { Id = 6, Name = "node6", Next = node7 }; Node node5 = new() { Id = 5, Name = "node5", Next = node6 }; Node node4 = new() { Id = 4, Name = "node4", Next = node5 }; Node node3 = new() { Id = 3, Name = "node3", Next = node4 }; Node node2 = new() { Id = 2, Name = "node2", Next = node3 }; Node node1 = new() { Id = 1, Name = "node1", Next = node2 }; node9.Next = node1; // 形成环 var referenceJson = JsonSerializer.Serialize(dto, new JsonSerializerOptions{    ReferenceHandler = ReferenceHandler.Preserve,     WriteIndented = true }); referenceJson.Display(); 
  • 如果以上代码不配置ReferenceHandler会报错
  • 异常信息为A possible object cycle was detected...

7. Poco循环引用处理的代码

7.1 Menu转DTO代码如下

(Func<Menu, MenuDTO>)((Menu source) => //MenuDTO {     MenuDTO dest = null;     if ((source != (Menu)null))     {         dest = new MenuDTO();         List<Menu> Children = null;         dest.Id = source.Id;         dest.Name = source.Name;         dest.Description = source.Description;         Children = source.Children;         if ((Children != null))         {             dest.Children = default(CompiledConverter<List<Menu>, List<MenuDTO>>)/*NOTE: Provide the non-default value for the Constant!*/.Convert(Children);         }     }     return dest; }); 

7.2 List<Menu>转DTO代码如下

(Func<List<Menu>, List<MenuDTO>>)((List<Menu> source) => //List<MenuDTO> {     List<MenuDTO> dest = null;     if ((source != (List<Menu>)null))     {         dest = new List<MenuDTO>(source.Count);         int index = default;         int len = default;         index = 0;         len = source.Count;         while (true)         {             if ((index < len))             {                 Menu sourceItem = null;                 MenuDTO destItem = null;                 sourceItem = source[index];                 // { The block result will be assigned to `destItem`                     MenuDTO dest_1 = null;                     destItem = ((Func<Menu, MenuDTO>)((Menu source_1) => //MenuDTO                     {                         MenuDTO dest_2 = null;                         if ((source_1 != (Menu)null))                         {                             dest_2 = new MenuDTO();                             List<Menu> Children = null;                             dest_2.Id = source_1.Id;                             dest_2.Name = source_1.Name;                             dest_2.Description = source_1.Description;                             Children = source_1.Children;                             if ((Children != null))                             {                                 dest_2.Children = default(CompiledConverter<List<Menu>, List<MenuDTO>>)/*NOTE: Provide the non-default value for the Constant!*/.Convert(Children);                             }                         }                         return dest_2;                     }))                     .Invoke(                         sourceItem);                     // } end of block assignment;                 dest.Add(destItem);                 index++;             }             else             {                 goto forLabel;             }         }         forLabel:;     }     return dest; }); 

8. AutoMapper和Poco生成代码对比

  • AutoMapper生成代码量是Poco的3倍多
  • AutoMapper生成的代码可读性不好,Poco生成的代码几乎就是正常程序员手写代码
  • CompiledConverter.Convert对应AutoMapper的context.MapInternal
  • 本case中Poco无多余缓存处理,节省了大量cpu和内存
  • 如果有对象循环引用Poco该怎么办呢

三、再举个环形链表的Case

  • 链表是类型循环引用
  • 环形链表又是对象循环引用
  • 中国传统有九九归一的说法,以此为例

1. 九九归一代码

public class Node {     public int Id { get; set; }     public string Name { get; set; }     public Node Next { get; set; }     public static Node GetNode()     {         Node node9 = new() { Id = 9, Name = "node9" };         Node node8 = new() { Id = 8, Name = "node8", Next = node9 };         Node node7 = new() { Id = 7, Name = "node7", Next = node8 };         Node node6 = new() { Id = 6, Name = "node6", Next = node7 };         Node node5 = new() { Id = 5, Name = "node5", Next = node6 };         Node node4 = new() { Id = 4, Name = "node4", Next = node5 };         Node node3 = new() { Id = 3, Name = "node3", Next = node4 };         Node node2 = new() { Id = 2, Name = "node2", Next = node3 };         Node node1 = new() { Id = 1, Name = "node1", Next = node2 };         node9.Next = node1; // 形成环         return node1;     } } 

2. PocoEmit配置缓存解决对象循环引用问题

  • ComplexCached.Circle表示只有检测到循环引用才开启缓存
  • ComplexCached.Circle策略基本等同AutoMapper
  • 默认是ComplexCached.Never,不开启缓存
var node = Node.GetNode(); var manager = PocoEmit.Mapper.Create(new MapperOptions { Cached = ComplexCached.Circle }); var dto = manager.Convert<Node, NodeDTO>(node); 

2.1 执行效果如下:

{   "$id": "1",   "Id": 1,   "Name": "node1",   "Next": {     "$id": "2",     "Id": 2,     "Name": "node2",     "Next": {       "$id": "3",       "Id": 3,       "Name": "node3",       "Next": {         "$id": "4",         "Id": 4,         "Name": "node4",         "Next": {           "$id": "5",           "Id": 5,           "Name": "node5",           "Next": {             "$id": "6",             "Id": 6,             "Name": "node6",             "Next": {               "$id": "7",               "Id": 7,               "Name": "node7",               "Next": {                 "$id": "8",                 "Id": 8,                 "Name": "node8",                 "Next": {                   "$id": "9",                   "Id": 9,                   "Name": "node9",                   "Next": {                     "$ref": "1"                   }                 }               }             }           }         }       }     }   } } 

2.2 与AutoMapper性能对比如下

Method Mean Error StdDev Median Ratio RatioSD Gen0 Gen1 Allocated Alloc Ratio
Auto 678.3 ns 12.65 ns 14.06 ns 666.1 ns 1.78 0.04 0.0936 0.0004 1616 B 4.49
AutoFunc 632.7 ns 4.18 ns 4.64 ns 628.8 ns 1.66 0.02 0.0936 0.0004 1616 B 4.49
Poco 381.8 ns 2.66 ns 3.07 ns 382.0 ns 1.00 0.01 0.0208 - 360 B 1.00
PocoFunc 365.4 ns 2.73 ns 2.92 ns 366.9 ns 0.96 0.01 0.0208 - 360 B 1.00
  • 首先可以看出Poco和AutoMapper执行耗时都挺高的
  • 所以建议大家使用AutoMapper尽量避免类型循环引用
  • 使用Poco也建议大家尽量避免对象循环引用
  • Poco性能好不少,差不多2倍
  • 内存分配上Poco优势更明显,AutoMapper分配了4倍多的内存

3. PocoEmit还可以通过GetContextConvertFunc来控制对象缓存

3.1 GetContextConvertFunc调用代码

  • GetContextConvertFunc是强制开启缓存,忽略mapper的缓存配置
  • 并设置当前类型必须缓存
var node = Node.GetNode(); var manager = PocoEmit.Mapper.Create(); Func<IConvertContext, Node, NodeDTO> contextFunc = manager.GetContextConvertFunc<Node, NodeDTO>(); using var context = SingleContext<Node, NodeDTO>.Pool.Get(); var dto = _pocoContextFunc(context, _node); 
  • 需要特别强调,context是用来做缓存的,不是专门用来做处理循环引用的
  • 巧的是缓存能解决对象循环引用问题
  • 缓存除了处理对象循环引用当然还有其他用处

3.2 加入GetContextConvertFunc对比如下

Method Mean Error StdDev Median Ratio RatioSD Gen0 Gen1 Allocated Alloc Ratio
Auto 657.2 ns 8.03 ns 8.92 ns 664.7 ns 1.81 0.04 0.0936 0.0004 1616 B 4.49
AutoFunc 621.6 ns 8.09 ns 8.65 ns 614.4 ns 1.71 0.04 0.0936 0.0004 1616 B 4.49
Poco 363.5 ns 6.60 ns 7.60 ns 359.6 ns 1.00 0.03 0.0208 - 360 B 1.00
PocoFunc 349.1 ns 1.51 ns 1.74 ns 348.8 ns 0.96 0.02 0.0208 - 360 B 1.00
PocoContextFunc 350.8 ns 3.15 ns 3.63 ns 350.0 ns 0.97 0.02 0.0208 - 360 B 1.00
  • PocoContextFunc性能和GetConvertFunc差不多
  • 主要影响性能的是缓存的读写
  • 该方法通过暴露IConvertContext参数给自定义和配置提供了想象空间
  • IConvertContext作为参数还可以多个方法调用共享,实现更magic的效果
  • 还可以通过实现IConvertContext来实现想要的逻辑和性能

四、重复引用非循环的Case

1. 一个突击小组的代码

  • 小组只有3人
  • 1人做队长
  • 1人做通讯员
public class SoldierTeam {     public Soldier Leader { get; set; }     public Soldier Courier { get; set; }     public List<Soldier> Members { get; set; }     public static SoldierTeam GetTeam()     {         var leader = new Soldier { Name = "张三" };         var courier = new Soldier { Name = "李四" };         var other = new Soldier { Name = "王二" };         var team = new SoldierTeam         {             Leader = leader,             Courier = courier,             Members = new List<Soldier>             {                 leader,                 courier,                 other             }         };         return team;     } } public class Soldier {     public string Name { get; set; } } 

2. Poco默认情况下转化为5个对象

  • 这明显并不是用户想要的结果
  • AutoMapper可以通过PreserveReferences配置跟踪引用(就是缓存)
var manager = PocoEmit.Mapper.Create()     .UseCollection(); var team = SoldierTeam.GetTeam(); var dto = manager.Convert<SoldierTeam, SoldierTeamDTO>(team); // dtoList.Length == 5 var dtoList = dto.Members.Concat([dto.Leader, dto.Courier]).Distinct().ToArray(); 

3. Poco配置缓存可以解决问题

  • ComplexCached.Always表示可能需要缓存就开启
  • 实际是检测有类被属性多次引用就开启缓存
  • 或有循环引用也开启缓存

3.1 Poco缓存转化代码

var manager = PocoEmit.Mapper.Create(new MapperOptions { Cached = ComplexCached.Always })     .UseCollection(); var team = SoldierTeam.GetTeam(); var dto = manager.Convert<SoldierTeam, SoldierTeamDTO>(team); // dtoList.Length == 3 var dtoList = dto.Members.Concat([dto.Leader, dto.Courier]).Distinct().ToArray(); 

3.2 Poco生成以下代码

T __f<T>(System.Func<T> f) => f(); (Func<SoldierTeam, SoldierTeamDTO>)((SoldierTeam source) => //SoldierTeamDTO {     SoldierTeamDTO dest = null;     IConvertContext context = null;     if ((source != (SoldierTeam)null))     {         context = ConvertContext.Create();         if ((source != (SoldierTeam)null))         {             dest = new SoldierTeamDTO();             context.SetCache<SoldierTeam, SoldierTeamDTO>(                 source,                 dest);             Soldier Leader = null;             Soldier Courier = null;             List<Soldier> Members = null;             Leader = source.Leader;             if ((Leader != null))             {                 // { The block result will be assigned to `dest.Leader`                 SoldierDTO dest_1 = null;                 dest.Leader = context.TryGetCache<Soldier, SoldierDTO>(                     Leader,                     out dest_1) ? dest_1 :                      __f(() => {                         SoldierDTO dest_2 = null;                         if ((Leader != (Soldier)null))                         {                             dest_2 = new SoldierDTO();                             context.SetCache<Soldier, SoldierDTO>(                                 Leader,                                 dest_2);                             dest_2.Name = Leader.Name;                         }                         return dest_2;                     });                 // } end of block assignment;             }             Courier = source.Courier;             if ((Courier != null))             {                 // { The block result will be assigned to `dest.Courier`                 SoldierDTO dest_3 = null;                 dest.Courier = context.TryGetCache<Soldier, SoldierDTO>(                     Courier,                     out dest_3) ? dest_3 :                      __f(() => {                         SoldierDTO dest_4 = null;                         if ((Courier != (Soldier)null))                         {                             dest_4 = new SoldierDTO();                             context.SetCache<Soldier, SoldierDTO>(                                 Courier,                                 dest_4);                             dest_4.Name = Courier.Name;                         }                         return dest_4;                     });                 // } end of block assignment;             }             Members = source.Members;             if ((Members != null))             {                 // { The block result will be assigned to `dest.Members`                 List<SoldierDTO> dest_5 = null;                 dest.Members = context.TryGetCache<List<Soldier>, List<SoldierDTO>>(                     Members,                     out dest_5) ? dest_5 :                      __f(() => {                         List<SoldierDTO> dest_6 = null;                         if ((Members != (List<Soldier>)null))                         {                             dest_6 = new List<SoldierDTO>(Members.Count);                             context.SetCache<List<Soldier>, List<SoldierDTO>>(                                 Members,                                 dest_6);                             int index = default;                             int len = default;                             index = 0;                             len = Members.Count;                             while (true)                             {                                 if ((index < len))                                 {                                     Soldier sourceItem = null;                                     SoldierDTO destItem = null;                                     sourceItem = Members[index];                                     // { The block result will be assigned to `destItem`                                         SoldierDTO dest_7 = null;                                         destItem = context.TryGetCache<Soldier, SoldierDTO>(                                             sourceItem,                                             out dest_7) ? dest_7 :                                              __f(() => {                                                 SoldierDTO dest_8 = null;                                                 if ((sourceItem != (Soldier)null))                                                 {                                                     dest_8 = new SoldierDTO();                                                     context.SetCache<Soldier, SoldierDTO>(                                                         sourceItem,                                                         dest_8);                                                     dest_8.Name = sourceItem.Name;                                                 }                                                 return dest_8;                                             });                                         // } end of block assignment;                                     dest_6.Add(destItem);                                     index++;                                 }                                 else                                 {                                     goto forLabel;                                 }                             }                             forLabel:;                         }                         return dest_6;                     });                 // } end of block assignment;             }         }         context.Dispose();     }     return dest; }); 

4. Poco通过GetContextConvertFunc也可以处理

4.1 GetContextConvertFunc转化代码

var manager = PocoEmit.Mapper.Create()     .UseCollection(); var team = SoldierTeam.GetTeam(); Func<IConvertContext, SoldierTeam, SoldierTeamDTO> func = manager.GetContextConvertFunc<SoldierTeam, SoldierTeamDTO>(); using var context = SingleContext<Soldier, SoldierDTO>.Pool.Get(); var dto = func(context, team); // dtoList.Length == 3 var dtoList = dto.Members.Concat([dto.Leader, dto.Courier]).Distinct().ToArray(); 

4.2 Poco生成以下代码

T __f<T>(System.Func<T> f) => f(); (Func<IConvertContext, SoldierTeam, SoldierTeamDTO>)((     IConvertContext context,      SoldierTeam source) => //SoldierTeamDTO {     SoldierTeamDTO dest = null;     if ((source != (SoldierTeam)null))     {         dest = new SoldierTeamDTO();         context.SetCache<SoldierTeam, SoldierTeamDTO>(             source,             dest);         Soldier Leader = null;         Soldier Courier = null;         List<Soldier> Members = null;         Leader = source.Leader;         if ((Leader != null))         {             // { The block result will be assigned to `dest.Leader`             SoldierDTO dest_1 = null;             dest.Leader = context.TryGetCache<Soldier, SoldierDTO>(                 Leader,                 out dest_1) ? dest_1 :                  __f(() => {                     SoldierDTO dest_2 = null;                     if ((Leader != (Soldier)null))                     {                         dest_2 = new SoldierDTO();                         context.SetCache<Soldier, SoldierDTO>(                             Leader,                             dest_2);                         dest_2.Name = Leader.Name;                     }                     return dest_2;                 });             // } end of block assignment;         }         Courier = source.Courier;         if ((Courier != null))         {             // { The block result will be assigned to `dest.Courier`             SoldierDTO dest_3 = null;             dest.Courier = context.TryGetCache<Soldier, SoldierDTO>(                 Courier,                 out dest_3) ? dest_3 :                  __f(() => {                     SoldierDTO dest_4 = null;                     if ((Courier != (Soldier)null))                     {                         dest_4 = new SoldierDTO();                         context.SetCache<Soldier, SoldierDTO>(                             Courier,                             dest_4);                         dest_4.Name = Courier.Name;                     }                     return dest_4;                 });             // } end of block assignment;         }         Members = source.Members;         if ((Members != null))         {             // { The block result will be assigned to `dest.Members`             List<SoldierDTO> dest_5 = null;             dest.Members = context.TryGetCache<List<Soldier>, List<SoldierDTO>>(                 Members,                 out dest_5) ? dest_5 :                  __f(() => {                     List<SoldierDTO> dest_6 = null;                     if ((Members != (List<Soldier>)null))                     {                         dest_6 = new List<SoldierDTO>(Members.Count);                         context.SetCache<List<Soldier>, List<SoldierDTO>>(                             Members,                             dest_6);                         int index = default;                         int len = default;                         index = 0;                         len = Members.Count;                         while (true)                         {                             if ((index < len))                             {                                 Soldier sourceItem = null;                                 SoldierDTO destItem = null;                                 sourceItem = Members[index];                                 // { The block result will be assigned to `destItem`                                     SoldierDTO dest_7 = null;                                     destItem = context.TryGetCache<Soldier, SoldierDTO>(                                         sourceItem,                                         out dest_7) ? dest_7 :                                          __f(() => {                                             SoldierDTO dest_8 = null;                                             if ((sourceItem != (Soldier)null))                                             {                                                 dest_8 = new SoldierDTO();                                                 context.SetCache<Soldier, SoldierDTO>(                                                     sourceItem,                                                     dest_8);                                                 dest_8.Name = sourceItem.Name;                                             }                                             return dest_8;                                         });                                     // } end of block assignment;                                 dest_6.Add(destItem);                                 index++;                             }                             else                             {                                 goto forLabel;                             }                         }                         forLabel:;                     }                     return dest_6;                 });             // } end of block assignment;         }     }     return dest; }); 

5. 性能测试如下

Method Mean Error StdDev Ratio RatioSD Gen0 Allocated Alloc Ratio
Auto 306.8 ns 10.60 ns 12.21 ns 1.39 0.06 0.0459 792 B 4.12
AutoFunc 259.1 ns 1.32 ns 1.47 ns 1.18 0.02 0.0459 792 B 4.12
Poco 220.2 ns 2.95 ns 3.39 ns 1.00 0.02 0.0111 192 B 1.00
PocoFunc 206.8 ns 2.26 ns 2.61 ns 0.94 0.02 0.0111 192 B 1.00
PocoContextFunc 207.4 ns 2.74 ns 3.15 ns 0.94 0.02 0.0111 192 B 1.00
  • PocoFunc性能和PocoContextFunc性能差不多
  • 如果喜欢隔离配置的同学,可以使用缓存配置方案
  • 如果喜欢集中配置的同学,可以使用GetContextConvertFunc
  • AutoMapper耗时1.4倍,内存占用4倍多

五、总结

1. 与AutoMapper处理循环引用的原理是一样的

  • 用其他对象调用,代替当时尚未编译的代码处理编译死循环
  • 使用缓存解决执行死循环
  • 缓存操作比原本对象转化耗时多太多,请大家慎用缓存

2. AutoMapper处理粗犷一点

  • 所有对象转化都加上下文对象,哪怕完全用不上
  • 检测到循环引用就加读写缓存,拖累到性能
  • 用了AutoMapper如果感觉获取数据慢,可以查一下是否有循环引用
  • 如果AutoMapper转化数据比实际数据库操作还慢也不要太过惊讶
  • 这里链接园内大佬的一篇文章: https://www.cnblogs.com/dudu/p/5863042.html

3. Poco处理就细致的多

  • 只有需要时才加上下文
  • 上下文来自内存池,用完回收复用,节约内存
  • 用户可以通过配置或GetContextConvertFunc选择性开启缓存
  • 自定义IConvertContext可以提供更多想象的空间
  • 另外无论是否开启缓存,Poco的性能都优于AutoMapper

另外源码托管地址: https://github.com/donetsoftwork/MyEmit ,欢迎大家直接查看源码。
gitee同步更新:https://gitee.com/donetsoftwork/MyEmit

如果大家喜欢请动动您发财的小手手帮忙点一下Star,谢谢!!!

发表评论

评论已关闭。

相关文章