在C#中最快的方法做浅拷贝
我想知道在C#中进行浅拷贝的最快方法是什么? 我只知道有两种方法可以做浅拷贝:
- MemberwiseClone
- 逐个复制每个字段(手动)
我发现(2)比(1)更快。 我想知道是否有另一种方法来做浅拷贝?
这是一个复杂的问题,有很多可能的解决scheme,每个方面都有很多优缺点。 这里有一篇精彩的文章,概述了在C#中制作副本的几种不同方法。 总结:
-
手动克隆
单调乏味,但控制水平很高。 -
使用MemberwiseClone进行复制
只创build一个浅拷贝,即对于引用types字段,原始对象及其克隆引用同一个对象。 -
克隆与reflection
浅拷贝默认情况下,可以重新写入做深拷贝。 优点:自动化。 缺点:反思慢。 -
克隆序列化
简单,自动化。 放弃一些控制和序列化是最慢的。 -
用IL克隆,用扩展方法克隆
更先进的解决scheme,不常见。
我很困惑。 MemberwiseClone()
应该消除浅拷贝的任何其他性能。 在CLI中,RCW以外的任何types应该能够通过以下顺序被浅拷贝:
- 在types的托儿所分配内存。
- 从原始的数据
memcpy
到新的。 由于目标是在托儿所,所以不需要任何写入屏障。 - 如果对象具有用户定义的终结器,则将其添加到未完成的项目的GC列表中。
- 如果源对象具有调用
SuppressFinalize
并且这样的标志存储在对象头中,则将其置于克隆中。
- 如果源对象具有调用
CLR内部团队中的某个人可以解释为什么事实并非如此?
我想从几个引号开始:
事实上,MemberwiseClone通常比别人好,特别是复杂types。
和
我很困惑。 MemberwiseClone()应该消除浅拷贝的任何其他性能。 […]
理论上,浅拷贝的最佳实现是C ++拷贝构造函数:它知道编译时的大小,然后对所有字段进行成员级的克隆。 下一个最好的事情是使用memcpy
或类似的,这基本上是如何MemberwiseClone
应该工作。 这意味着,理论上它应该抹杀所有其他performance的可能性。 对?
…但显然它不是快速的,它并没有抹杀所有其他的解决scheme。 在底部,我已经发布了一个解决scheme,速度超过了2倍。 所以: 错了。
testingMemberwiseClone的内部
让我们从一个简单的blittabletypes的小testing开始,检查这里关于性能的基本假设:
[StructLayout(LayoutKind.Sequential)] public class ShallowCloneTest { public int Foo; public long Bar; public ShallowCloneTest Clone() { return (ShallowCloneTest)base.MemberwiseClone(); } }
testing的devise方式是我们可以检查MemberwiseClone
agaist原始memcpy
的性能,这是可能的,因为这是一个blittabletypes。
要自己testing,使用不安全的代码进行编译,禁用JIT抑制,编译释放模式并testing。 我也把时间安排在每一个相关的行之后。
实施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);
基本上我多次运行这些testing,检查程序集输出,以确保事情没有被优化,等等。最终的结果是,我知道这一行代码花费了多less时间,这是0.40s我的电脑。 这是我们使用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);
如果仔细观察这些数字,你会发现一些事情:
- 创build一个对象并复制它将花费大约0.20秒。 在正常情况下,这是可能的最快的代码。
- 但是,要做到这一点,你需要钉住和解除对象。 这会花费你0.81秒。
那为什么这么慢呢?
我的解释是,它与GC有关。 基本上,实现不能依赖于内存在完整GC之前和之后保持不变(在GC期间可以更改内存地址,这可能在任何时候发生,包括在浅拷贝期间)。 这意味着你只有两个可能的select:
- 固定数据并做一个副本。 请注意,
GCHandle.Alloc
只是其中一种方法,众所周知,像C ++ / CLI这样的东西会给你提供更好的性能。 - 列举字段。 这将确保在GC收集之间不需要做任何事情,并且在GC收集期间,您可以使用GCfunction修改移动对象堆栈上的地址。
MemberwiseClone
将使用方法1,这意味着由于钉扎程序,您将获得性能上的优势。
(更快)的实施
在所有情况下,我们的非托pipe代码都不能对这些types的大小做出假设,并且必须对数据进行固定。 对大小进行假设可以使编译器做更好的优化,比如循环展开,寄存器分配等等(就像C ++ copy ctor比memcpy
)。 不必拼数据意味着我们不会得到额外的性能影响。 由于.NET JIT的汇编程序,理论上这意味着我们应该能够使用简单的IL发射来实现更快的实现,并允许编译器对其进行优化。
那么总结一下为什么这个比原生实现更快呢?
- 它不需要固定对象; 移动的物体由GC处理 – 实际上,这是无情的优化。
- 它可以对要复制的结构的大小做出假设,并因此允许更好的寄存器分配,循环展开等等。
我们想要的是原始的memcpy
或更好的性能:0.17s。
要做到这一点,我们基本上不能只使用一个call
,创build对象,并执行一堆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); } }
我已经testing了这个代码的结果:0.16s。 这意味着它比MemberwiseClone
快大约2.5倍。
更重要的是,这个速度与memcpy
相当,这或多或less是“正常情况下的最佳解决scheme”。
就我个人而言,我认为这是最快的解决scheme – 最好的部分是:如果.NET运行时将会变得更快(对SSE指令的适当支持等),这个解决scheme也是如此。
为什么复杂的东西? 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(); }
这是一种使用dynamicIL生成的方法。 我在网上find了它:
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通常比别人好,特别是复杂types。
原因是:如果你手动创build一个副本,它必须调用其中一个types的构造函数,但是使用memberwise clone,我猜它只是复制一块内存。 对于那些types有非常昂贵的构造行为,成员克隆绝对是最好的方法。
我写了这样的types:{string A = Guid.NewGuid()。ToString()},我发现成员的克隆比创build一个新的实例和手动分配成员更快。
下面的代码的结果是:
手动拷贝: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; } } }
最后,我在这里提供我的代码:
#region 数据克隆/// <summary> /// 依据不同types所存储的克隆句柄集合/// </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> /// 根据指定的types,创build对应的克隆句柄/// </summary> /// <param name="type">数据types</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">数据的types</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">数据types</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需要较less的维护。 我不知道是否有默认的属性值有助于任何,也许如果可以忽略具有默认值的项目。