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

tep*_*tep 54 c# shallow-copy cloning

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

  1. MemberwiseClone
  2. 逐个复制每个字段(手动)

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

Nic*_*mas 72

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

  1. 克隆手动
    繁琐,但高水平的控制.

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

  3. 用反射克隆
    默认情况下可以重写,可以重写进行深层复制.优势:自动化.缺点:反射很慢.

  4. 使用序列化克隆
    轻松,自动化.放弃一些控制和序列化是最慢的.

  5. 用IL克隆,用扩展方法克隆
    更高级的解决方案,不常见.

  • `MemberwiseClone`确实做了一些额外的检查等,所以对于字段很少的对象,实际上可能会稍慢一些.无论如何,如果您确实遇到性能问题,那么您应该只关心它.不要优化"预感",个人资料.如果您正在进行如此多的克隆以使用`MemberwiseClone`出现问题,那么您应该使用结构而不是类.或者也许需要对架构进行更积极的改变:D (6认同)
  • @tep nope,MemberwiseClone应该是最快的.您的结果可能是由调试/发布模式或某种其他因素引起的. (5认同)
  • 我看到..在所有可用选项中,我认为(1)是最快的.谢谢! (2认同)

Sam*_*ell 28

我糊涂了.MemberwiseClone()应该消除浅层副本的其他任何表现.在CLI中,除RCW之外的任何类型都应该能够按以下顺序进行浅层复制:

  • 在托儿所中为类型分配内存.
  • memcpy从原始数据到新数据.由于目标位于托儿所,因此不需要写入障碍.
  • 如果对象具有用户定义的终结器,请将其添加到待完成的GC项列表中.
    • 如果源对象已SuppressFinalize调用它并且此标志存储在对象标头中,请在克隆中取消设置.

CLR内部团队成员可以解释为什么不是这样吗?

  • @ ta.speot.is,bah欺骗.提高是一个很好的观点,我们大多数人都在讨论这些事情以便更好地理解事情. (14认同)
  • @SamHaswell`MememberwiseClone`是一个`外部'调用,这意味着它需要固定.钉扎是一个相对缓慢的过程.我在下面的答案中公布了我的测试和结果的详细信息.我在这里没有原生呼叫基准数据,但我非常确定固定/本地互操作和额外的vtable调用(clone,sizeof)占用了开销的irt.你发现的memcpy. (5认同)

atl*_*ste 28

我想从几个引号开始:

事实上,MemberwiseClone通常比其他人好得多,特别是对于复杂类型.

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

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

......但显然它并不快速,并没有消除所有其他解决方案.在底部,我实际上发布了一个超过2倍的解决方案.所以:错了.

测试MemberwiseClone的内部

让我们从使用简单blittable类型的一点测试开始,检查这里有关性能的基本假设:

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

    public ShallowCloneTest Clone()
    {
        return (ShallowCloneTest)base.MemberwiseClone();
    }
}
Run Code Online (Sandbox Code Playgroud)

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

要自己测试,使用不安全的代码编译,禁用JIT抑制,编译发布模式并测试.我还把时间安排在每条相关的线之后.

实施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);
Run Code Online (Sandbox Code Playgroud)

基本上我经常运行这些测试,检查组件输出,以确保没有优化的东西,等等.最终的结果是,我知道这一行代码花费了多少秒,这是0.40秒我的电脑.这是我们的基线使用MemberwiseClone.

实施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);
Run Code Online (Sandbox Code Playgroud)

仔细观察这些数字,你会发现一些事情:

  • 创建一个对象并复制它将需要大约0.20秒.在正常情况下,这是您可以拥有的最快的代码.
  • 但是,要做到这一点,您需要固定和取消固定对象.这将花费你0.81秒.

那为什么所有这一切都这么慢?

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

  1. 固定数据并进行复制.请注意,这GCHandle.Alloc只是执行此操作的方法之一,众所周知,像C++/CLI这样的东西会为您提供更好的性能.
  2. 枚举字段.这将确保GC之间收集您不需要做任何花哨的事情,并且在GC收集期间,您可以使用GC功能修改移动对象堆栈上的地址.

MemberwiseClone 将使用方法1,这意味着由于固定过程,您将获得性能损失.

(更快)实施

在所有情况下,我们的非托管代码都不能对类型的大小做出假设,并且必须固定数据.对大小做出假设使编译器能够进行更好的优化,例如循环展开,寄存器分配等(就像C++拷贝ctor更快memcpy).无需固定数据意味着我们不会获得额外的性能影响.从.NET JIT到汇编程序,理论上这意味着我们应该能够使用简单的IL发射更快地实现,并允许编译器对其进行优化.

那么总结一下为什么这比原生实现更快?

  1. 它不需要固定物体; 移动的对象由GC处理 - 实际上,这是不断优化的.
  2. 它可以假设要复制的结构的大小,因此允许更好的寄存器分配,循环展开等.

我们的目标是原始memcpy或更好的表现:0.17s.

要做到这一点,我们基本上不能只使用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);
    }
}
Run Code Online (Sandbox Code Playgroud)

我已经用结果测试了这段代码:0.16s.这意味着它比它快大约2.5倍MemberwiseClone.

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

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


小智 14

为什么复杂的事情?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();
}
Run Code Online (Sandbox Code Playgroud)


eul*_*rfx 8

这是使用动态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);
    }
}
Run Code Online (Sandbox Code Playgroud)

  • @Noldorin不正确,它仅使用反射来创建DynamicMethod - 后续调用与手动克隆一样快.但是,这仍然比MemberwiseClone慢,当然假设.NET使用"memcpy"类型的操作正确实现它. (3认同)

dex*_*ang 5

事实上,MemberwiseClone通常比其他人好得多,特别是对于复杂类型.

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

Onece我写了这样的类型:{string A = Guid.NewGuid().ToString()},我发现成员克隆比创建新实例和手动分配成员更快.

下面的代码结果如下:

手动复印:00:00:00.0017099

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;
        }
    }

}
Run Code Online (Sandbox Code Playgroud)

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

    #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
Run Code Online (Sandbox Code Playgroud)