更快的深度克隆

26

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

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


你是在处理单个对象吗?还是对象树/图形? - Marc Gravell
对象树/图 - 就像我说的,不用担心重复引用,但是对象是嵌套的,即它们不仅包含平面值属性,还包含其他数据合同。 - Karol Kolenda
如果您需要最佳性能并且不介意额外生成的代码,请查看CGbR Code Generator - Toxantron
DeepCloner是由force-net开发的非常快速的克隆库(2021):https://github.com/force-net/DeepCloner - Sire
刚发现这个:https://github.com/ReubenBond/DeepCopy。它可以比我自己写的基于反射的怪物快约20倍(我的简单POCO对象)。 - mBardos
9个回答

24

我之前曾经为.NET写了三种深度克隆方法:

  • 其中一种使用众所周知的BinaryFormatter技术(虽然我对其进行了调整,以便不需要将对象序列化即可进行克隆)。这是迄今为止最慢的一种。

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

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

Logarithmic scale illustrating cloning performance

水平轴显示被克隆的对象数(虽然每个被克隆的对象包含几个嵌套对象)。

在图表中,BinaryFormatter标记为“Serialization”。数据系列“Reflection”是一个使用GetField()/SetField()复制字段的自定义系列。

我在此处发布了所有三种克隆方法的开源代码:

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


5
我很好奇protobuf-net的序列化与这三个相比如何。 - Patrick Szalapski
1
我使用ExpressionTreeCloner时遇到了System.Security.VerificationException: Operation could destabilize the runtime。有任何线索吗? - grzegorz_p
@PatrickSzalapski,我已经进行了一些基准测试。除去设置时间和文件系统访问之外,ProtoBuf-net的完成时间只有“ReflectionCloner”的2/3,并且比“ExpressionTreeCloner”慢5倍。 - Cygon
水平轴是对象的数量吗? - StayOnTarget
数据系列标记为“reflection”的是BinaryFormatter吗?谢谢。 - StayOnTarget
@DaveInCaz 是的,水平轴显示了克隆对象的数量(尽管每个克隆对象包含几个嵌套对象)。BinaryFormatter在这里标记为“序列化”。 “Reflection”是一个自定义的复制字段的方法,通过GetField()/SetField()实现。 - Cygon

15
如果你正在谈论一个对象树/图形:
编写特定的IL序列化对象是棘手的。依我之见,你最好查看完整的序列化,比如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);

如果你使用的是没有 Expression 的 .NET 2.0/3.0 版本,你可以考虑使用HyperDescriptor来实现类似的功能。


1
好的,到目前为止我已经测试了AutoMapper,它实际上比DataContract序列化慢4倍,来自The Instruction Limit的快速反射建议sambo99比DataContract快2倍,而protobuf比DataContract快3倍。Marc,请您详细说明一下:“当前(不完整的)正在进行中的工作提供了没有属性的POCO支持。”我无法在protobuf的特色版本中使用ProtoContract使其正常工作。谢谢。 - Karol Kolenda
是的,我在protobuf文档中读到了这个,但它似乎不起作用。我的意思是,当我没有指定任何内容时,我会得到一个例外,说合同是必需的。当我指定DataContract/DataMember时,会创建一个新对象,但什么也没有复制。当使用[ProtoContract(ImplicitFields=ImplicitFields.AllPublic)])时,一切都按预期工作。我感到困惑。 - Karol Kolenda
抱歉,Marc,我没有注意到你关于订单的评论。一切都运行正常。 - Karol Kolenda
@MarcGravell:我只是想知道您是否仍需要proto-buf属性才能使深度克隆起作用?我想在库中包含一个深度克隆,而不需要外部类知道proto-buf的任何开销。 - Ian
@Ian 如果是进程内的话,你可能会发现BinaryFormatter更简单。在protobuf-net中有一些基于约定的东西,它可以配置为在没有属性的情况下工作,但我不确定对于任意的“外部类”是否值得麻烦。 - Marc Gravell
显示剩余3条评论

10

有很多库可以进行此操作。您可以在这里查看基准测试结果:

简单来说,如果您需要性能,则需手动执行,它的速度更快。此外,一些库允许执行浅层克隆(根据问题,这是一个不错的选择),其速度更快。如果需要任何性能,请勿使用BinaryFormatter

@frakon还提到表达式树与IL Emit具有相同的速度,这略有不正确。表达式树稍微慢一些,但它可以用于部分受信任的应用程序。

手动 13ms

DeepCloner(IL Emit)167ms

DeepCloner(Expressions)267ms

CloneExtensions(Expressions)560ms

NClone 901ms

Clone.Behave! 8551ms

GeorgeCloney 1996ms

Nuclex.Cloning n / a(崩溃)

FastDeepCloner 1882ms

BinaryFormatter 15000ms


5

目前在互联网上可能没有使用IL Emit完整的克隆代码。

但是,IL Emit的速度与表达式树生成的代码相同,因为两种方法最终都会生成类似的编译lambda复制函数。表达式树生成的代码大约比反射快4倍。最好的事情是表达式树生成的通用克隆函数可以在互联网上找到

Cygon已经提到了一个由表达式树生成的实现(链接)。在CodeProject文章Fast Deep Copy by Expression Trees (C#)中可以找到新的经过彻底测试的实现。

使用扩展方法:

var copy = originalObject.DeepCopyByExpressionTree();

3

我不确定这是否完全符合你的要求,但你也可以使用BinaryFormatter创建一个深度克隆。请参见这个答案来了解相关问题(由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);
        }
    }
}

1
这比基于动态方法的序列化要慢得多,至少慢10倍。 - Sam Saffron
1
@sambo99:有性能比较的资料吗?我只找到了一个比较标准序列化方法的(http://developers.de/blogs/damir_dobric/archive/2007/08/05/performance-comparison-of-most-popular-serializes.aspx)。 - Dirk Vollmar
@divo 请参考:http://code.google.com/p/protobuf-net/wiki/Performance .. Marc在protobuf .net中使用基于动态方法的序列化。 - Sam Saffron
1
我刚刚在我的帖子中添加了自己对三种不同的深度克隆方法的基准测试结果。你的10倍慢的估计并没有太离谱 :) - Cygon

3

AutoMapper 不适合用于对象克隆,因为它的创始人本人已经指出:https://github.com/AutoMapper/AutoMapper/issues/340 - urig

1

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

您可以为每个属性/字段设置1个方法或为整个对象设置1个方法。从我的基准测试来看,每个属性设置1个方法不会对性能造成太大影响。

请参见以下代码,了解我在Media Browser中如何实现此操作:http://code.google.com/p/videobrowser/source/browse/trunk/MediaBrowser/Library/Persistance/Serializer.cs

那里还有一些单元测试。

instructionlimit上有一个快速反射示例,正好符合您的要求。

请参见:

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


2
你的Serializer示例有新链接吗? - Ian

0

CGbR 代码生成器 可以为您生成 ICloneable 的实现。您只需要拥有 NuGet 包 和一个实现 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;
    }
}

-1

嘿!你可以编写自己的克隆方法,通过属性指定忽略或包括属性。

我在下面的链接中提供了我的新库,它使用反射和FieldInfo来递归克隆对象。

我已经将它添加到CodeProject,所以你很快就能访问它的代码,你可以根据自己的需求进行修改。

试用一下吧,它非常快速和干净,你会喜欢的。
https://www.nuget.org/packages/FastDeepCloner/1.0.1
或者
PM> Install-Package FastDeepCloner


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接