我想知道在 C# 中进行浅拷贝的最快方法是什么?我只知道有两种方法可以做浅拷贝:
- 会员克隆
- 一个一个地复制每个字段(手动)
我发现(2)比(1)快。我想知道是否有另一种方法来进行浅拷贝?
我想知道在 C# 中进行浅拷贝的最快方法是什么?我只知道有两种方法可以做浅拷贝:
我发现(2)比(1)快。我想知道是否有另一种方法来进行浅拷贝?
这是一个复杂的主题,有很多可能的解决方案,每个解决方案都有许多优点和缺点。这里有一篇精彩的文章,概述了在 C# 中制作副本的几种不同方法。总结一下:
手动克隆
繁琐,但控制水平很高。
使用 MemberwiseClone 克隆
只创建一个浅拷贝,即对于引用类型字段,原始对象及其克隆引用同一个对象。
克隆默认使用反射
浅拷贝,可以重写做深拷贝。优点:自动化。缺点:反射慢。
使用序列化克隆
简单、自动化。放弃一些控制,序列化是最慢的。
使用 IL 进行克隆,使用扩展方法进行克隆
更高级的解决方案,并不常见。
我想从几句话开始:
事实上,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();
    }
}
测试的设计方式是我们可以检查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);
基本上我多次运行这些测试,检查程序集输出以确保没有优化,等等。最终结果是我知道这一行代码大约花费了多少秒,即 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);
如果你仔细观察这些数字,你会注意到一些事情:
那么为什么这一切都这么慢呢?
我的解释是它与GC有关。基本上,实现不能依赖于内存在完整 GC 之前和之后保持不变的事实(内存地址可以在 GC 期间更改,这可能随时发生,包括在您的浅拷贝期间)。这意味着您只有 2 个可能的选项:
GCHandle.Alloc只是其中一种方法,众所周知,C++/CLI 之类的东西会给您带来更好的性能。MemberwiseClone将使用方法 1,这意味着您会因为固定过程而受到性能影响。
(快得多)的实施
在所有情况下,我们的非托管代码都无法对类型的大小做出假设,它必须固定数据。对大小进行假设使编译器能够进行更好的优化,例如循环展开、寄存器分配等(就像 C++ 复制 ctor 比 ctor 更快memcpy)。不必固定数据意味着我们不会受到额外的性能影响。由于 .NET JIT 是汇编程序,理论上这意味着我们应该能够使用简单的 IL 发射来更快地实现,并允许编译器对其进行优化。
那么总结一下为什么这可以比本机实现更快?
我们的目标是原始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);
    }
}
我用结果测试了这段代码:0.16s。这意味着它比MemberwiseClone.
更重要的是,这个速度与 相当memcpy,或多或少是“正常情况下的最优解”。
就个人而言,我认为这是最快的解决方案——最好的部分是:如果 .NET 运行时会变得更快(对 SSE 指令等的适当支持),那么这个解决方案也会变得更快。
编者按:
上面的示例代码假定默认构造函数是公共的。如果不是,则调用GetConstructor返回 null。在这种情况下,请使用其他GetConstructor签名之一来获取受保护或私有构造函数。请参阅https://docs.microsoft.com/en-us/dotnet/api/system.type.getconstructor?view=netframework-4.8
我很困惑。MemberwiseClone()应该消除浅拷贝的其他任何东西的性能。在 CLI 中,除了 RCW 之外的任何类型都应该能够通过以下序列进行浅拷贝:
memcpy从原始数据到新数据。由于目标在托儿所中,因此不需要写屏障。SuppressFinalize调用了它并且这样的标志存储在对象头中,则在克隆中取消设置它。CLR 内部团队中的某个人可以解释为什么不是这种情况吗?
为什么要把事情复杂化?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();
}
这是一种使用动态 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);
    }
}
事实上,MemberwiseClone 通常比其他的要好得多,尤其是对于复杂类型。
原因是:如果你手动创建一个副本,它必须调用一个类型的构造函数,但是使用成员克隆,我猜它只是复制一块内存。对于那些类型具有非常昂贵的构造操作,按成员克隆绝对是最好的方法。
我曾经写过这样的类型:{string A = Guid.NewGuid().ToString()},我发现按成员克隆比创建新实例和手动分配成员要快得多。
以下代码的结果:
手动复制:00:00:00.0017099
会员克隆: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;
        }
    }
}
最后,我在这里提供我的代码:
    #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
MemberwiseClone 需要较少的维护。我不知道拥有默认属性值是否有帮助,也许是否可以忽略具有默认值的项目。
这是一个小的帮助类,它使用反射来访问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);
}
你可以这样称呼它:
Person b = a.ShallowClone();