题
我想知道在 C# 中进行浅复制的最快方法是什么?我只知道有两种方法可以进行浅复制:
- 会员克隆
- 逐一复制每个字段(手动)
我发现(2)比(1)更快。我想知道是否还有其他方法进行浅复制?
解决方案
这是一个复杂的主题,有许多可能的解决方案,并且每种解决方案都有很多优点和缺点。有一篇精彩的文章 这里 概述了在 C# 中制作副本的几种不同方法。总结一下:
手动克隆
乏味,但控制水平高。使用 MemberwiseClone 进行克隆
只创建一个浅拷贝,即对于引用类型字段,原始对象及其克隆引用同一对象。带反射的克隆
默认为浅拷贝,可以重写做深拷贝。优势:自动化。坏处:反射很慢。克隆与序列化
简单、自动化。放弃一些控制,序列化是最慢的。使用 IL 克隆、使用扩展方法克隆
更先进的解决方案,并不常见。
其他提示
我很困惑。 MemberwiseClone()
应该 歼灭 浅拷贝的其他任何表现。在 CLI 中,除 RCW 之外的任何类型都应该能够按以下顺序进行浅复制:
- 在托儿所中为该类型分配内存。
memcpy
数据从原始数据到新数据。由于目标位于托儿所中,因此不需要写屏障。- 如果对象具有用户定义的终结器,请将其添加到 GC 待终结项列表中。
- 如果源对象有
SuppressFinalize
调用它并且这样的标志存储在对象标头中,在克隆中取消设置它。
- 如果源对象有
CLR 内部团队的有人可以解释为什么情况并非如此吗?
我想先引用几句话:
事实上,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();
}
}
该测试的设计方式使我们可以检查 MemberwiseClone
反对生的 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);
如果仔细观察这些数字,您会注意到以下几点:
- 创建一个对象并复制它大约需要 0.20 秒。在正常情况下,这是您可以拥有的最快的代码。
- 但是,为此,您需要固定和取消固定对象。这将花费您 0.81 秒。
那么为什么这一切都这么慢呢?
我的解释是它与GC有关。基本上,实现不能依赖于内存在完整 GC 之前和之后保持不变的事实(内存的地址可以在 GC 期间更改,这可能随时发生,包括在浅复制期间)。这意味着您只有 2 个可能的选择:
- 固定数据并进行复制。注意
GCHandle.Alloc
只是实现此目的的方法之一,众所周知,C++/CLI 之类的东西会给您带来更好的性能。 - 枚举字段。这将确保在 GC 收集之间您不需要做任何花哨的事情,并且在 GC 收集期间您可以使用 GC 功能来修改移动对象堆栈上的地址。
MemberwiseClone
将使用方法 1,这意味着您将因固定过程而受到性能影响。
(更快)更快的实施
在所有情况下,我们的非托管代码都无法对类型的大小做出假设,并且必须固定数据。对大小进行假设使编译器能够进行更好的优化,例如循环展开、寄存器分配等。(就像 C++ 复制构造函数比 memcpy
)。不必固定数据意味着我们不会受到额外的性能影响。由于 .NET JIT 是针对汇编程序的,理论上这意味着我们应该能够使用简单的 IL 发出来更快地实现,并允许编译器对其进行优化。
那么总结一下为什么这比本机实现更快?
- 它不需要固定对象;移动的对象由 GC 处理——实际上,这是不断优化的。
- 它可以对要复制的结构的大小进行假设,因此允许更好的寄存器分配、循环展开等。
我们的目标是原始的性能 memcpy
或更好:0.17秒。
为此,我们基本上只能使用 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.16秒。这意味着它的速度大约是 2.5 倍 MemberwiseClone
.
更重要的是,这个速度与 memcpy
, ,这或多或少是“正常情况下的最佳解决方案”。
就我个人而言,我认为这是最快的解决方案 - 最好的部分是:如果 .NET 运行时会变得更快(对 SSE 指令等的适当支持),那么这个解决方案也会变得更快。
为什么要把事情复杂化呢?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();