在 C# 中进行浅拷贝的最快方法

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/966451/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-06 04:24:48  来源:igfitidea点击:

Fastest Way to do Shallow Copy in C#

c#cloningshallow-copy

提问by tep

I wonder what is the fastest way to do shallow copying in C#? I only know there are 2 ways to do shallow copy:

我想知道在 C# 中进行浅复制的最快方法是什么?我只知道有两种方法可以进行浅拷贝:

  1. MemberwiseClone
  2. Copy each field one by one (manual)
  1. 会员克隆
  2. 一一复制每个字段(手动)

I found that (2) is faster than (1). I'm wondering if there's another way to do shallow copying?

我发现(2)比(1)快。我想知道是否有另一种方法可以进行浅复制?

采纳答案by Nick Stamas

This is a complex subject with lots of possible solutions and many pros and cons to each. There is a wonderful article herethat outlines several different ways of making a copy in C#. To summarize:

这是一个复杂的主题,有很多可能的解决方案,每个解决方案都有很多优点和缺点。有一个精彩的文章在这里用C#创建副本的,概述了几种不同的方式。总结一下:

  1. Clone Manually
    Tedious, but high level of control.

  2. Clone with MemberwiseClone
    Only creates a shallow copy, i.e. for reference-type fields the original object and its clone refer to the same object.

  3. Clone with Reflection
    Shallow copy by default, can be re-written to do deep copy. Advantage: automated. Disadvantage: reflection is slow.

  4. Clone with Serialization
    Easy, automated. Give up some control and serialization is slowest of all.

  5. Clone with IL, Clone with Extension Methods
    More advanced solutions, not as common.

  1. 手动克隆
    繁琐,但高度控制。

  2. 使用 MemberwiseClone 进行克隆
    仅创建浅拷贝,即对于引用类型字段,原始对象及其克隆引用同一个对象。

  3. Clone with Reflection
    默认浅拷贝,可以重写做深拷贝。优点:自动化。缺点:反射慢。

  4. 带有序列化的克隆
    简单、自动化。放弃一些控制,序列化是最慢的。

  5. Clone with IL, Clone with Extension Methods
    更高级的解决方案,不那么常见。

回答by eulerfx

This is a way to do it using dynamic IL generation. I found it somewhere online:

这是一种使用动态 IL 生成的方法。我在网上某处找到了它:

public static class Cloner
{
    static Dictionary<Type, Delegate> _cachedIL = new Dictionary<Type, Delegate>();

    public static T Clone<T>(T myObject)
    {
        Delegate myExec = null;

        if (!_cachedIL.TryGetValue(typeof(T), out myExec))
        {
            var dymMethod = new DynamicMethod("DoClone", typeof(T), new Type[] { typeof(T) }, true);
            var cInfo = myObject.GetType().GetConstructor(new Type[] { });

            var generator = dymMethod.GetILGenerator();

            var lbf = generator.DeclareLocal(typeof(T));

            generator.Emit(OpCodes.Newobj, cInfo);
            generator.Emit(OpCodes.Stloc_0);

            foreach (var field in myObject.GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
            {
                // Load the new object on the eval stack... (currently 1 item on eval stack)
                generator.Emit(OpCodes.Ldloc_0);
                // Load initial object (parameter)          (currently 2 items on eval stack)
                generator.Emit(OpCodes.Ldarg_0);
                // Replace value by field value             (still currently 2 items on eval stack)
                generator.Emit(OpCodes.Ldfld, field);
                // Store the value of the top on the eval stack into the object underneath that value on the value stack.
                //  (0 items on eval stack)
                generator.Emit(OpCodes.Stfld, field);
            }

            // Load new constructed obj on eval stack -> 1 item on stack
            generator.Emit(OpCodes.Ldloc_0);
            // Return constructed object.   --> 0 items on stack
            generator.Emit(OpCodes.Ret);

            myExec = dymMethod.CreateDelegate(typeof(Func<T, T>));

            _cachedIL.Add(typeof(T), myExec);
        }

        return ((Func<T, T>)myExec)(myObject);
    }
}

回答by Eric Schneider

MemberwiseClone requires less maintenance. I don't know if having default property values helps any, maybe if could ignore items with default values.

MemberwiseClone 需要较少的维护。我不知道拥有默认属性值是否有帮助,也许是否可以忽略具有默认值的项目。

回答by Sam Harwell

I'm confused. MemberwiseClone()should annihilatethe performance of anything else for a shallow copy. In the CLI, any type other than an RCW should be able to be shallow-copied by the following sequence:

我糊涂了。MemberwiseClone()应该为浅拷贝消灭其他任何东西的性能。在 CLI 中,除了 RCW 之外的任何类型都应该能够通过以下序列进行浅复制:

  • Allocate memory in the nursery for the type.
  • memcpythe data from the original to the new. Since the target is in the nursery, no write barriers are required.
  • If the object has a user-defined finalizer, add it to the GC list of items pending finalization.
    • If the source object has SuppressFinalizecalled on it and such a flag is stored in the object header, unset it in the clone.
  • 在托儿所中为类型分配内存。
  • memcpy从原始数据到新数据。由于目标在托儿所中,因此不需要写屏障。
  • 如果对象具有用户定义的终结器,请将其添加到待终结项目的 GC 列表中。
    • 如果源对象SuppressFinalize调用了它并且这样的标志存储在对象头中,则在克隆中取消设置它。

Can someone on the CLR internals team explain why this is not the case?

CLR 内部团队中的某个人能否解释为什么情况并非如此?

回答by jun estevez

Why complicate things? MemberwiseClone would suffice.

为什么要把事情复杂化?MemberwiseClone 就足够了。

public class ClassA : ICloneable
{
   public object Clone()
   {
      return this.MemberwiseClone();
   }
}

// let's say you want to copy the value (not reference) of the member of that class.
public class Main()
{
    ClassA myClassB = new ClassA();
    ClassA myClassC = new ClassA();
    myClassB = (ClassA) myClassC.Clone();
}

回答by dexiang

In fact, MemberwiseClone is usually much better than others, especially for complex type.

事实上,MemberwiseClone 通常比其他的要好得多,尤其是对于复杂类型。

The reason is that:if you manual create a copy, it must call one of the type's constructor, but use memberwise clone, I guess it just copy a block of memory. for those types has very expensive construct actions, memberwise clone is absolutely the best way.

原因是:如果你手动创建一个副本,它必须调用类型的构造函数之一,但使用成员克隆,我猜它只是复制一块内存。对于那些具有非常昂贵的构造操作的类型,成员克隆绝对是最好的方法。

Onece i wrote such type: {string A = Guid.NewGuid().ToString()}, I found memberwise clone is muct faster than create a new instance and manual assign members.

我曾经写过这样的类型:{string A = Guid.NewGuid().ToString()},我发现按成员克隆比创建新实例和手动分配成员要快很多。

The code below's result:

下面的代码的结果:

Manual Copy:00:00:00.0017099

手动复印:00:00:00.0017099

MemberwiseClone:00:00:00.0009911

MemberwiseClone:00:00:00.0009911

namespace MoeCard.TestConsole
{
    class Program
    {
        static void Main(string[] args)
        {
            Program p = new Program() { AAA = Guid.NewGuid().ToString(), BBB = 123 };
            Stopwatch sw = Stopwatch.StartNew();
            for (int i = 0; i < 10000; i++)
            {
                p.Copy1();
            }
            sw.Stop();
            Console.WriteLine("Manual Copy:" + sw.Elapsed);

            sw.Restart();
            for (int i = 0; i < 10000; i++)
            {
                p.Copy2();
            }
            sw.Stop();
            Console.WriteLine("MemberwiseClone:" + sw.Elapsed);
            Console.ReadLine();
        }

        public string AAA;

        public int BBB;

        public Class1 CCC = new Class1();

        public Program Copy1()
        {
            return new Program() { AAA = AAA, BBB = BBB, CCC = CCC };
        }
        public Program Copy2()
        {
            return this.MemberwiseClone() as Program;
        }

        public class Class1
        {
            public DateTime Date = DateTime.Now;
        }
    }

}

finally, I provide my code here:

最后,我在这里提供我的代码:

    #region 数据克隆
    /// <summary>
    /// 依据不同类型所存储的克隆句柄集合
    /// </summary>
    private static readonly Dictionary<Type, Func<object, object>> CloneHandlers = new Dictionary<Type, Func<object, object>>();

    /// <summary>
    /// 根据指定的实例,克隆一份新的实例
    /// </summary>
    /// <param name="source">待克隆的实例</param>
    /// <returns>被克隆的新的实例</returns>
    public static object CloneInstance(object source)
    {
        if (source == null)
        {
            return null;
        }
        Func<object, object> handler = TryGetOrAdd(CloneHandlers, source.GetType(), CreateCloneHandler);
        return handler(source);
    }

    /// <summary>
    /// 根据指定的类型,创建对应的克隆句柄
    /// </summary>
    /// <param name="type">数据类型</param>
    /// <returns>数据克隆句柄</returns>
    private static Func<object, object> CreateCloneHandler(Type type)
    {
        return Delegate.CreateDelegate(typeof(Func<object, object>), new Func<object, object>(CloneAs<object>).Method.GetGenericMethodDefinition().MakeGenericMethod(type)) as Func<object, object>;
    }

    /// <summary>
    /// 克隆一个类
    /// </summary>
    /// <typeparam name="TValue"></typeparam>
    /// <param name="value"></param>
    /// <returns></returns>
    private static object CloneAs<TValue>(object value)
    {
        return Copier<TValue>.Clone((TValue)value);
    }
    /// <summary>
    /// 生成一份指定数据的克隆体
    /// </summary>
    /// <typeparam name="TValue">数据的类型</typeparam>
    /// <param name="value">需要克隆的值</param>
    /// <returns>克隆后的数据</returns>
    public static TValue Clone<TValue>(TValue value)
    {
        if (value == null)
        {
            return value;
        }
        return Copier<TValue>.Clone(value);
    }

    /// <summary>
    /// 辅助类,完成数据克隆
    /// </summary>
    /// <typeparam name="TValue">数据类型</typeparam>
    private static class Copier<TValue>
    {
        /// <summary>
        /// 用于克隆的句柄
        /// </summary>
        internal static readonly Func<TValue, TValue> Clone;

        /// <summary>
        /// 初始化
        /// </summary>
        static Copier()
        {
            MethodFactory<Func<TValue, TValue>> method = MethodFactory.Create<Func<TValue, TValue>>();
            Type type = typeof(TValue);
            if (type == typeof(object))
            {
                method.LoadArg(0).Return();
                return;
            }
            switch (Type.GetTypeCode(type))
            {
                case TypeCode.Object:
                    if (type.IsClass)
                    {
                        method.LoadArg(0).Call(Reflector.GetMethod(typeof(object), "MemberwiseClone")).Cast(typeof(object), typeof(TValue)).Return();
                    }
                    else
                    {
                        method.LoadArg(0).Return();
                    }
                    break;
                default:
                    method.LoadArg(0).Return();
                    break;
            }
            Clone = method.Delegation;
        }

    }
    #endregion

回答by atlaste

I'd like to start with a few quotes:

我想先引用几句:

In fact, MemberwiseClone is usually much better than others, especially for complex type.

事实上,MemberwiseClone 通常比其他的要好得多,尤其是对于复杂类型。

and

I'm confused. MemberwiseClone() should annihilate the performance of anything else for a shallow copy. [...]

我糊涂了。MemberwiseClone() 应该消除任何其他浅拷贝的性能。[...]

Theoretically the best implementation of a shallow copy is a C++ copy constructor: it knowsthe size compile-time, and then does a memberwise clone of all fields. The next best thing is using memcpyor something similar, which is basically how MemberwiseCloneshould work. This means, in theory it should obliterate all other possibilities in terms of performance. Right?

理论上,浅拷贝的最佳实现是 C++ 拷贝构造函数:它知道编译时的大小,然后对所有字段进行成员克隆。下一个最好的事情是使用memcpy或类似的东西,这基本上是MemberwiseClone应该如何工作的。这意味着,理论上它应该消除性能方面的所有其他可能性。对?

... but apparently it isn't blazing fast and it doesn't obliterate all the other solutions. At the bottom I've actually posted a solution that's over 2x faster. So: Wrong.

...但显然它不是很快,也没有消除所有其他解决方案。在底部,我实际上发布了一个速度提高了 2 倍以上的解决方案。所以:错了。

Testing the internals of MemberwiseClone

测试 MemberwiseClone 的内部结构

Let's start with a little test using a simple blittable type to check the underlying assumptions here about performance:

让我们从使用简单 blittable 类型的小测试开始,以检查此处有关性能的基本假设:

[StructLayout(LayoutKind.Sequential)]
public class ShallowCloneTest
{
    public int Foo;
    public long Bar;

    public ShallowCloneTest Clone()
    {
        return (ShallowCloneTest)base.MemberwiseClone();
    }
}

The test is devised in such a way that we can check the performance of MemberwiseCloneagaist raw memcpy, which is possible because this is a blittable type.

该测试的设计方式是我们可以检查agaist MemberwiseCloneraw的性能memcpy,这是可能的,因为这是一种 blittable 类型。

To test by yourself, compile with unsafe code, disable the JIT suppression, compile release mode and test away. I've also put the timings after every line that's relevant.

要自己测试,请使用不安全的代码进行编译,禁用 JIT 抑制,编译发布模式并进行测试。我还在每一行相关的行之后放置了时间。

Implementation 1:

实施1

ShallowCloneTest t1 = new ShallowCloneTest() { Bar = 1, Foo = 2 };
Stopwatch sw = Stopwatch.StartNew();
int total = 0;
for (int i = 0; i < 10000000; ++i)
{
    var cloned = t1.Clone();                                    // 0.40s
    total += cloned.Foo;
}

Console.WriteLine("Took {0:0.00}s", sw.Elapsed.TotalSeconds);

Basically I ran these tests a number of times, checked the assembly output to ensure that the thing wasn't optimized away, etc. The end result is that I know approximately how much seconds this one line of code costs, which is 0.40s on my PC. This is our baseline using MemberwiseClone.

基本上我多次运行这些测试,检查汇编输出以确保事情没有被优化掉,等等。最终的结果是我知道这行代码大约需要多少秒,即 0.40 秒我的电脑。这是我们使用MemberwiseClone.

Implementation 2:

实施2

sw = Stopwatch.StartNew();

total = 0;
uint bytes = (uint)Marshal.SizeOf(t1.GetType());
GCHandle handle1 = GCHandle.Alloc(t1, GCHandleType.Pinned);
IntPtr ptr1 = handle1.AddrOfPinnedObject();

for (int i = 0; i < 10000000; ++i)
{
    ShallowCloneTest t2 = new ShallowCloneTest();               // 0.03s
    GCHandle handle2 = GCHandle.Alloc(t2, GCHandleType.Pinned); // 0.75s (+ 'Free' call)
    IntPtr ptr2 = handle2.AddrOfPinnedObject();                 // 0.06s
    memcpy(ptr2, ptr1, new UIntPtr(bytes));                     // 0.17s
    handle2.Free();

    total += t2.Foo;
}

handle1.Free();
Console.WriteLine("Took {0:0.00}s", sw.Elapsed.TotalSeconds);

If you look closely at these numbers, you'll notice a few things:

如果仔细观察这些数字,您会注意到以下几点:

  • Creating an object and copying it will take roughly 0.20s. Under normal circumstances this is the fastest possible code you can have.
  • However, to do that, you need to pin and unpin the object. That will take you 0.81 seconds.
  • 创建一个对象并复制它大约需要 0.20 秒。在正常情况下,这是您可以拥有的最快的代码。
  • 但是,要做到这一点,您需要固定和取消固定对象。这将花费您 0.81 秒。

So why is all of this so slow?

那么为什么这一切都如此缓慢?

My explanation is that it has to do with the GC. Basically the implementations cannot rely on the fact that memory will stay the same before and after a full GC (The address of the memory can be changed during a GC, which can happen at any moment, including during your shallow copy). This means you only have 2 possible options:

我的解释是它与GC有关。基本上,实现不能依赖于内存在完整 GC 之前和之后保持不变的事实(内存地址可以在 GC 期间更改,这可能随时发生,包括在您的浅拷贝期间)。这意味着您只有两种可能的选择:

  1. Pinning the data and doing a copy. Note that GCHandle.Allocis just one of the ways to do this, it's well known that things like C++/CLI will give you better performance.
  2. Enumerating the fields. This will ensure that between GC collects you don't need to do anything fancy, and during GC collects you can use the GC ability to modify the addresses on the stack of moved objects.
  1. 固定数据并进行复制。请注意,这GCHandle.Alloc只是其中一种方法,众所周知,C++/CLI 之类的东西会给您带来更好的性能。
  2. 枚举字段。这将确保在 GC 收集之间您不需要做任何花哨的事情,并且在 GC 收集期间您可以使用 GC 功能修改移动对象堆栈上的地址。

MemberwiseClonewill use method 1, which means you'll get a performance hit because of the pinning procedure.

MemberwiseClone将使用方法 1,这意味着您将因固定过程而受到性能影响。

A (much) faster implementation

一个(快得多)更快的实现

In all cases our unmanaged code cannot make assumptions about the size of the types and it has to pin data. Making assumptions about size enables the compiler to do better optimizations, like loop unrolling, register allocation, etc. (just like a C++ copy ctor is faster than memcpy). Not having to pin data means we don't get an extra performance hit. Since .NET JIT's to assembler, in theory this means that we should be able to make a faster implementation using simple IL emitting, and allowing the compiler to optimize it.

在所有情况下,我们的非托管代码都不能对类型的大小做出假设,它必须固定数据。对大小进行假设使编译器能够进行更好的优化,例如循环展开、寄存器分配等(就像 C++ 复制构造函数比 快一样memcpy)。不必固定数据意味着我们不会受到额外的性能影响。由于 .NET JIT 是汇编器,理论上这意味着我们应该能够使用简单的 IL 发射来实现更快的实现,并允许编译器对其进行优化。

So to summarize on why this can be faster than the native implementation?

那么总结一下为什么这可以比本机实现更快?

  1. It doesn't require the object to be pinned; objects that are moving around are handled by the GC -- and really, this is relentlessly optimized.
  2. It can make assumptions about the size of the structure to copy, and therefore allows for better register allocation, loop unrolling, etc.
  1. 它不需要固定对象;移动的对象由 GC 处理——实际上,这是无情的优化。
  2. 它可以假设要复制的结构的大小,因此允许更好的寄存器分配、循环展开等。

What we're aiming for is the performance of raw memcpyor better: 0.17s.

我们的目标是原始memcpy或更好的性能:0.17s。

To do that, we basically cannot use more than just a call, create the object, and perform a bunch of copyinstructions. It looks a bit like the Clonerimplementation above, but some important differences (most significant: no Dictionaryand no redundant CreateDelegatecalls). Here goes:

要做到这一点,我们基本上不能只使用 a call,创建对象并执行一堆copy指令。它看起来有点像Cloner上面的实现,但有一些重要的区别(最重要的是:没有Dictionary和没有冗余CreateDelegate调用)。开始:

public static class Cloner<T>
{
    private static Func<T, T> cloner = CreateCloner();

    private static Func<T, T> CreateCloner()
    {
        var cloneMethod = new DynamicMethod("CloneImplementation", typeof(T), new Type[] { typeof(T) }, true);
        var defaultCtor = typeof(T).GetConstructor(new Type[] { });

        var generator = cloneMethod .GetILGenerator();

        var loc1 = generator.DeclareLocal(typeof(T));

        generator.Emit(OpCodes.Newobj, defaultCtor);
        generator.Emit(OpCodes.Stloc, loc1);

        foreach (var field in typeof(T).GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
        {
            generator.Emit(OpCodes.Ldloc, loc1);
            generator.Emit(OpCodes.Ldarg_0);
            generator.Emit(OpCodes.Ldfld, field);
            generator.Emit(OpCodes.Stfld, field);
        }

        generator.Emit(OpCodes.Ldloc, loc1);
        generator.Emit(OpCodes.Ret);

        return ((Func<T, T>)cloneMethod.CreateDelegate(typeof(Func<T, T>)));
    }

    public static T Clone(T myObject)
    {
        return cloner(myObject);
    }
}

I've tested this code with the result: 0.16s. This means it's approximately 2.5x faster than MemberwiseClone.

我已经用结果测试了这段代码:0.16s。这意味着它比MemberwiseClone.

More importantly, this speed is on-par with memcpy, which is more or less the 'optimal solution under normal circumstances'.

更重要的是,这个速度与 相当memcpy,这或多或少是“正常情况下的最佳解决方案”。

Personally, I think this is the fastest solution - and the best part is: if the .NET runtime will get faster (proper support for SSE instructions etc), so will this solution.

就个人而言,我认为这是最快的解决方案 - 最好的部分是:如果 .NET 运行时会变得更快(对 SSE 指令的适当支持等),那么这个解决方案也会变得更快。

Editorial Note:The sample code above assumes that the default constructor is public. If it is not, the call to GetConstructorreturns null. In that case, use one of the other GetConstructorsignatures to obtain protected or private constructors. See https://docs.microsoft.com/en-us/dotnet/api/system.type.getconstructor?view=netframework-4.8

编者按:上面的示例代码假定默认构造函数是公共的。如果不是,则调用GetConstructor返回 null。在这种情况下,使用其他GetConstructor签名之一来获取受保护或私有的构造函数。请参阅https://docs.microsoft.com/en-us/dotnet/api/system.type.getconstructor?view=netframework-4.8

回答by Tim Pohlmann

Here is a small helper class that uses reflection to access MemberwiseCloneand then caches the delegate to avoid using reflection more than necessary.

这是一个小的帮助器类,它使用反射来访问MemberwiseClone然后缓存委托以避免不必要地使用反射。

public static class CloneUtil<T>
{
    private static readonly Func<T, object> clone;

    static CloneUtil()
    {
        var cloneMethod = typeof(T).GetMethod("MemberwiseClone", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
        clone = (Func<T, object>)cloneMethod.CreateDelegate(typeof(Func<T, object>));
    }

    public static T ShallowClone(T obj) => (T)clone(obj);
}

public static class CloneUtil
{
    public static T ShallowClone<T>(this T obj) => CloneUtil<T>.ShallowClone(obj);
}

You can call it like this:

你可以这样称呼它:

Person b = a.ShallowClone();