【问题标题】:Faster deep cloning更快的深度克隆
【发布时间】:2010-10-25 12:32:14
【问题描述】:

有没有人想要一个允许我按值克隆的框架/类 .Net 对象?我只对公共读/写属性(即 DataContracts)感兴趣,我不在乎引用是否正确解析(即包含两次相同项目实例的集合)。

我尝试通过DataContractSerializer 进行序列化技巧(序列化为 XML 并返回),编写了基于反射的克隆类(有时更快/有时更慢),并且想知道是否有人编写了一个可以通过 Emit 而不是反射。至于现在发出 IL 对我的小脑袋来说有点过分了,但我想这将是最终的解决方案。除非有人知道比 DataContractSerializer 更快的替代方法。

【问题讨论】:

  • 您在处理单个对象吗?还是对象树/图?
  • 对象树/图——就像我说的,不关心重复引用,但对象是嵌套的,即它们不仅包含平面值属性,还包含其他数据协定。
  • 如果您需要尽可能好的性能并且不介意生成额外的代码,请查看CGbR Code Generator
  • force-net 的 DeepCloner 速度极快(2021 年):github.com/force-net/DeepCloner

标签: c# .net


【解决方案1】:

我前段时间为.NET写了三个深度克隆方法:

  • 使用众所周知的BinaryFormatter 技术(尽管我对其进行了调整,以便对象不需要为了被克隆而可序列化)。这是迄今为止最慢的。

  • 第二次我使用了纯反射。它比使用BinaryFormatter 克隆至少快 6 倍。这个也可以用于 Silverlight 和 .NET Compact Framework。

  • 第三个使用 Linq 表达式树(用于运行时 MSIL 生成)。它比 BinaryFormatter 技术快 60 倍,但第一次遇到每个类时的设置时间约为 2 毫秒。

水平轴显示克隆对象的数量(尽管每个克隆对象都包含多个嵌套对象)。

BinaryFormatter 在图表中被标记为“序列化”。数据系列“Reflection”是通过GetField()/SetField()复制字段的自定义系列。

我在这里将所有三种克隆方法作为开源发布:

http://blog.nuclex-games.com/mono-dotnet/fast-deep-cloning/

【讨论】:

  • 我很好奇 protobuf-net 的序列化总线与这三个相比如何。
  • 我收到 System.Security.VerificationException:操作可能会破坏运行时的稳定性。使用 ExpressionTreeCloner。有什么线索吗?
  • @PatrickSzalapski 我已经做了一些基准测试。排除设置时间和文件系统访问,ProtoBuf-net 完成时间是ReflectionCloner 的 2/3,仅比ExpressionTreeCloner 慢 5 倍。
  • 横轴是对象数吗?
  • 标记为“反射”的数据系列是 BinaryFormatter 的吗?谢谢
【解决方案2】:

如果您在谈论对象树/图:

编写特定的 IL 来序列化一个对象是很棘手的。 IMO,最好的办法是查看完整的序列化,例如 DataContractSerializer 的工作方式 - 但不一定与该引擎一起使用。

例如,protobuf-net 有一个 Serializer.DeepClone<T> 方法可能会有所帮助。至少它应该比DataContractSerializer 快。目前,您需要为序列化程序添加一些线索(即使只是[ProtoContract(ImplicitFields=ImplicitFields.AllPublic)]) - 但是,当前(不完整的)正在进行的工作提供了不带属性的 POCO 支持。


如果您谈论的是单个对象:

您可以在 .NET 3.5 中使用 Expression 执行相当简单的操作;基于反射构建动态Expression,并调用.Compile()MiscUtil 已经有这个了:

DestType clone = PropertyCopy<DestType>.CopyFrom(original);

对于 .NET 2.0/3.0(不带 Expression),您可能会出于类似目的考虑使用 HyperDescriptor

【讨论】:

  • 好的,到目前为止,我已经测试了 AutoMapper,它实际上比 DataContract 序列化慢 4 倍,sambo99 建议的指令限制的快速反射比 DataContract 快 2 倍,protobuf 快 3 倍。 Marc,您能否详细说明一下:“当前(不完整的)进行中的工作提供了没有属性的 POCO 支持。”如果没有来自 protobuf 的特色版本的 ProtoContract,我无法使其工作。谢谢。
  • 我确实说过它不完整!基本上,“lessgenerics”分支(还不能工作;不要尝试)有一个单独的机制来定义成员等 - 显然 defaulting 将属性作为后备。所以在未来的某个时候你可以通过说“像这样对待这种类型......”来使用封闭源类型 - 有点像 XmlSerializer 的大规模重载 ctor。
  • 其实,如果你的数据合约指定了每个成员的订单,那么它会很高兴地使用 [DataContract]/[DataMember]...
  • 是的,我在 protobuf 文档中读到了这个,但它不知何故不起作用。我的意思是当我没有指定任何内容时,我有一个例外,即合同是必要的。当我指定 DataContract/DataMember 时,会创建一个新对象,但不会复制任何内容。当使用 [ProtoContract(ImplicitFields=ImplicitFields.AllPublic)]) 时,一切都按预期工作。我很困惑。
  • 抱歉,Marc,没有注意到您关于 Order 的评论。一切正常。
【解决方案3】:

有很多库可以执行此操作。可以看到基准测试结果here

简而言之,如果您需要性能,请手动执行,它确实更快。此外,一些库允许执行 shallow 克隆(根据问题,这对您来说是一个很好的变体),这更快。如果您需要任何性能,请不要使用BinaryFormatter

另外,@frakon 提到表达式树与 IL Emit 具有相同的速度,这有点不正确。表达式树稍慢,但可以在部分受信任的应用中使用。

手动 13 毫秒

DeepCloner(IL 发射)167 毫秒

DeepCloner(表达式)267 毫秒

CloneExtensions(表达式)560ms

NClone 901ms

克隆。行为! 8551毫秒

乔治克隆尼 1996 毫秒

Nuclex.Cloning 不适用(崩溃)

FastDeepCloner 1882 毫秒

BinaryFormatter 15000ms

【讨论】:

    【解决方案4】:

    互联网上可能没有 IL Emit 制作的完整的工作克隆代码。

    但是 IL Emit 与 Expression Trees 的代码具有相同的速度,因为这两种方法最终都具有相似的编译 lambda 复制函数。表达式树大约比反射快 4 倍。最棒的是Expression Trees通用克隆功能可在网上获得

    Cygon 的表达式树 was already mentioned 的一个实现。可以在 CodeProject 文章中找到经过全面测试的新实现 Fast Deep Copy by Expression Trees (C#)

    使用扩展方法

    var copy = originalObject.DeepCopyByExpressionTree();
    

    【讨论】:

      【解决方案5】:

      【讨论】:

      【解决方案6】:

      我不知道这是否完全符合您的要求,但您也可以使用 BinaryFormatter 创建深度克隆。请参阅this answer 到相关问题(Binoj Antony):

      public static class GenericCopier<T>
      {
          public static T DeepCopy(object objectToCopy)
          {
              using (MemoryStream memoryStream = new MemoryStream())
              {
                  BinaryFormatter binaryFormatter = new BinaryFormatter();
                  binaryFormatter.Serialize(memoryStream, objectToCopy);
                  memoryStream.Seek(0, SeekOrigin.Begin);
                  return (T) binaryFormatter.Deserialize(memoryStream);
              }
          }
      }
      

      【讨论】:

      • 这比基于动态方法的序列化要慢得多。 (至少慢 10 倍左右)
      • @sambo99:有性能比较吗?我只发现了一种比较标准的序列化方法(developers.de/blogs/damir_dobric/archive/2007/08/05/…
      • @divo 请参阅:code.google.com/p/protobuf-net/wiki/Performance .. Marc 在 protobuf .net 中使用基于动态方法的序列化
      • 我刚刚在我的帖子中添加了我自己针对三种不同深度克隆方法的基准测试结果。你的估计慢了 10 倍,离你不远了 :)
      【解决方案7】:

      基于动态方法的序列化将是最快的。 (使用轻量级codegen生成动态方法并用于序列化)

      您可以为每个属性/字段执行 1 种方法或为整个对象执行一种方法。从我的基准测试来看,每个属性做 1 个并不会给你带来太多的性能影响。

      查看以下代码,了解我如何在媒体浏览器中执行此操作:http://code.google.com/p/videobrowser/source/browse/trunk/MediaBrowser/Library/Persistance/Serializer.cs

      那里也有一些单元测试。

      指令限制上有快速反射示例,可以完全满足您的要求。

      见:

      http://theinstructionlimit.com/?p=76

      【讨论】:

      • 您的序列化程序示例有新链接吗?
      【解决方案8】:

      CGbR Code Generator 可以为您生成ICloneable 的实现。您只需要nuget package 和实现ICloneable 的部分类定义。生成器将为您完成剩下的工作:

      public partial class Root : ICloneable
      {
          public Root(int number)
          {
              _number = number;
          }
          private int _number;
      
          public Partial[] Partials { get; set; }
      
          public IList<ulong> Numbers { get; set; }
      
          public object Clone()
          {
              return Clone(true);
          }
      
          private Root()
          {
          }
      } 
      
      public partial class Root
      {
          public Root Clone(bool deep)
          {
              var copy = new Root();
              // All value types can be simply copied
              copy._number = _number; 
              if (deep)
              {
                  // In a deep clone the references are cloned 
                  var tempPartials = new Partial[Partials.Length];
                  for (var i = 0; i < Partials.Length; i++)
                  {
                      var value = Partials[i];
                      value = value.Clone(true);
                      tempPartials[i] = value;
                  }
                  copy.Partials = tempPartials;
                  var tempNumbers = new List<ulong>(Numbers.Count);
                  for (var i = 0; i < Numbers.Count; i++)
                  {
                      var value = Numbers[i];
                      tempNumbers[i] = value;
                  }
                  copy.Numbers = tempNumbers;
              }
              else
              {
                  // In a shallow clone only references are copied
                  copy.Partials = Partials; 
                  copy.Numbers = Numbers; 
              }
              return copy;
          }
      }
      

      【讨论】:

        【解决方案9】:

        好吧!您可以编写自己的克隆方法,您可以指定通过其属性忽略或包含属性。 我在链接中的新库,使用反射和 FieldInfo 递归地克隆对象。 我已将其添加到 CodeProject,因此您很快就可以访问其代码,您可以根据需要对其进行修改。

        尝试一下它非常快速和干净,你会喜欢它。
        https://www.nuget.org/packages/FastDeepCloner/1.0.1
        要么
        PM> 安装包FastDeepCloner

        【讨论】:

          猜你喜欢
          • 2010-10-15
          • 1970-01-01
          • 1970-01-01
          • 2017-09-01
          • 2010-09-09
          • 1970-01-01
          • 2011-08-13
          相关资源
          最近更新 更多