c# cloning shallow-copy

La forma más rápida de hacer una copia superficial en C#



cloning shallow-copy (7)

Me pregunto cuál es la forma más rápida de hacer copias superficiales en C #? Solo sé que hay 2 formas de hacer una copia superficial:

  1. MemberwiseClone
  2. Copie cada campo uno por uno (manual)

Descubrí que (2) es más rápido que (1). Me pregunto si hay otra forma de hacer copias superficiales.


¿Por qué complicar las cosas? MemberwiseClone sería suficiente.

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


De hecho, MemberwiseClone suele ser mucho mejor que otros, especialmente para el tipo complejo.

La razón es que: si crea una copia manualmente, debe llamar a uno de los constructores del tipo, pero use clonar para miembros, supongo que solo copia un bloque de memoria. para esos tipos tiene acciones de construcción muy costosas, la clonación de miembros es absolutamente la mejor manera.

Onece escribí dicho tipo: {string A = Guid.NewGuid (). ToString ()}, encontré que el clonaje memberwise es mucho más rápido que crear una nueva instancia y asignar miembros manualmente.

El resultado del código a continuación:

Copia manual: 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; } } }

finalmente, proporciono mi código aquí:

#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


Esta es una forma de hacerlo utilizando la generación dinámica de IL. Lo encontré en algún lugar en línea:

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


Este es un tema complejo con muchas soluciones posibles y muchos pros y contras para cada uno. Aquí hay un artículo maravilloso que describe varias formas diferentes de hacer una copia en C #. Para resumir:

  1. Clonar manualmente
    Tedioso, pero alto nivel de control.

  2. Clonar con MemberwiseClone
    Solo crea una copia superficial, es decir, para los campos de tipo referencia, el objeto original y su clon se refieren al mismo objeto.

  3. Clonar con reflexión
    Copia superficial por defecto, se puede volver a escribir para hacer una copia en profundidad. Ventaja: automatizado. Desventaja: la reflexión es lenta.

  4. Clonar con serialización
    Fácil, automatizado Renunciar a un cierto control y la serialización es la más lenta de todas.

  5. Clonar con IL, clonar con métodos de extensión
    Soluciones más avanzadas, no tan comunes.


Estoy confundido. MemberwiseClone() debería aniquilar el rendimiento de cualquier otra cosa para una copia poco profunda. En la CLI, cualquier tipo que no sea un RCW debería poder copiarse poco a poco mediante la siguiente secuencia:

  • Asigne memoria en el vivero para el tipo.
  • memcpy los datos del original al nuevo. Como el objetivo está en el vivero, no se requieren barreras de escritura.
  • Si el objeto tiene un finalizador definido por el usuario, agréguelo a la lista de elementos pendientes de finalizar de la GC.
    • Si el objeto de origen tiene SuppressFinalize invocado y dicho indicador está almacenado en el encabezado del objeto, desconéctelo en el clon.

¿Puede alguien en el equipo interno de CLR explicar por qué este no es el caso?


Me gustaría comenzar con algunas citas:

De hecho, MemberwiseClone suele ser mucho mejor que otros, especialmente para el tipo complejo.

y

Estoy confundido. MemberwiseClone () debería aniquilar el rendimiento de cualquier otra cosa para una copia poco profunda. [...]

Teóricamente, la mejor implementación de una copia superficial es un constructor de copia C ++: conoce el tiempo de compilación del tamaño y luego realiza una clonación de todos los campos en forma de miembro. La mejor memcpy es usar memcpy o algo similar, que es básicamente cómo debería funcionar MemberwiseClone . Esto significa que, en teoría, debería eliminar todas las otras posibilidades en términos de rendimiento. ¿Derecha?

... pero aparentemente no está ardiendo rápido y no borra todas las demás soluciones. En la parte inferior, he publicado una solución que es más de 2 veces más rápida. Entonces: equivocado

Prueba de las partes internas de MemberwiseClone

Comencemos con una pequeña prueba usando un tipo blittable simple para verificar las suposiciones subyacentes aquí sobre el rendimiento:

[StructLayout(LayoutKind.Sequential)] public class ShallowCloneTest { public int Foo; public long Bar; public ShallowCloneTest Clone() { return (ShallowCloneTest)base.MemberwiseClone(); } }

La prueba está diseñada de tal manera que podemos verificar el rendimiento de MemberwiseClone agaist raw memcpy , que es posible porque es un tipo blittable.

Para probarlo usted mismo, compilar con código inseguro, desactivar la supresión de JIT, compilar el modo de lanzamiento y probarlo. También coloqué los tiempos después de cada línea relevante.

Implementación 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);

Básicamente, realicé estas pruebas varias veces, verifiqué la salida del ensamblaje para asegurarme de que la cosa no estaba optimizada, etc. El resultado final es que sé aproximadamente cuántos segundos cuesta esta línea de código, que es 0.40 en mi PC. Esta es nuestra línea base usando MemberwiseClone .

Implementación 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);

Si miras detenidamente estos números, notarás algunas cosas:

  • Crear un objeto y copiarlo tomará aproximadamente 0.20s. En circunstancias normales, este es el código más rápido posible que puede tener.
  • Sin embargo, para hacer eso, necesita fijar y desanclar el objeto. Eso te llevará 0.81 segundos.

Entonces, ¿por qué todo esto es tan lento?

Mi explicación es que tiene que ver con el GC. Básicamente, las implementaciones no pueden basarse en el hecho de que la memoria permanecerá igual antes y después de un GC completo (la dirección de la memoria puede cambiarse durante un GC, lo que puede suceder en cualquier momento, incluso durante su copia poco profunda). Esto significa que solo tienes 2 opciones posibles:

  1. Fijando los datos y haciendo una copia. Tenga en cuenta que GCHandle.Alloc es solo una de las formas de hacerlo, es bien sabido que cosas como C ++ / CLI le proporcionarán un mejor rendimiento.
  2. Enumerando los campos. Esto asegurará que entre GC coleccione usted no necesite hacer cualquier cosa de lujo, y durante la recolección de GC puede usar la capacidad de GC para modificar las direcciones en la pila de objetos movidos.

MemberwiseClone utilizará el método 1, lo que significa que obtendrá un golpe de rendimiento debido al procedimiento de fijación.

Una implementación (mucho) más rápida

En todos los casos, nuestro código no administrado no puede hacer suposiciones sobre el tamaño de los tipos y tiene que anclar datos. Hacer suposiciones sobre el tamaño permite al compilador realizar mejores optimizaciones, como desenrollar bucles, asignar registros, etc. (al igual que una copia de C ++ es más rápida que memcpy ). No tener que anclar datos significa que no recibimos un golpe de rendimiento adicional. Dado que .NET JIT es ensamblador, en teoría esto significa que deberíamos ser capaces de realizar una implementación más rápida mediante la emisión simple de IL y permitir que el compilador la optimice.

Entonces, ¿para resumir por qué esto puede ser más rápido que la implementación nativa?

  1. No requiere que se fije el objeto; los objetos que se mueven son manejados por el GC, y realmente, esto está implacablemente optimizado.
  2. Puede hacer suposiciones sobre el tamaño de la estructura para copiar y, por lo tanto, permite una mejor asignación de registros, desenrollado de bucles, etc.

Lo que estamos buscando es el rendimiento de memcpy bruto o mejor: 0.17s.

Para hacer eso, básicamente no podemos usar más que solo una call , crear el objeto y realizar un montón de instrucciones de copy . Se parece un poco a la implementación de Cloner anterior, pero algunas diferencias importantes (más significativas: sin Dictionary y sin llamadas de CreateDelegate redundantes). Aquí va:

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

Probé este código con el resultado: 0.16s. Esto significa que es aproximadamente 2.5 veces más rápido que MemberwiseClone .

Más importante aún, esta velocidad está a la par con memcpy , que es más o menos la ''solución óptima en circunstancias normales''.

Personalmente, creo que esta es la solución más rápida, y la mejor parte es: si el tiempo de ejecución de .NET será más rápido (soporte adecuado para las instrucciones de SSE, etc.), también lo hará esta solución.


MemberwiseClone requiere menos mantenimiento. No sé si tener valores de propiedad predeterminados ayuda, quizás si podría ignorar elementos con valores predeterminados.