Pregunta

Me pregunto ¿cuál es la manera más rápida de hacer poco profundas copiar en C#?Sólo sé que hay 2 maneras de hacer copia superficial:

  1. MemberwiseClone
  2. Copia de cada campo, uno por uno (manual)

He encontrado que (2) es más rápido que (1).Me pregunto si hay otra manera de hacer copia superficial?

¿Fue útil?

Solución

Este es un tema complejo con muchas de las posibles soluciones y muchos de los pros y los contras de cada uno.No es un artículo maravilloso aquí que esboza varias maneras diferentes de hacer una copia en C#.Para resumir:

  1. Clon Manualmente
    Tedioso, pero el alto nivel de control.

  2. Clon con MemberwiseClone
    Sólo crea una copia superficial, es decir,para el tipo de referencia de los campos del objeto original y su copia se refieren al mismo objeto.

  3. Clon con Reflexión
    Copia superficial de forma predeterminada, puede ser re-escrito para hacer copia en profundidad.Ventaja:automatizado.Desventaja:la reflexión es lento.

  4. Clon con la Serialización
    Fácil y automatizada.Ceder parte del control y la serialización es el más lento de todos.

  5. Clon con IL, Clon con los Métodos de Extensión
    Soluciones más avanzadas, no es tan común.

Otros consejos

Estoy confundido. MemberwiseClone() debe aniquilan el desempeño de cualquier otra cosa para una copia superficial. En la CLI, cualquier tipo distinto de un RCW debe ser capaz de ser superficial-copiado por la siguiente secuencia:

  • asignar memoria en el vivero para el tipo.
  • memcpy los datos de la original a la nueva. Dado que el objetivo está en la guardería, no se requieren barreras de escritura.
  • Si el objeto tiene un finalizador definida por el usuario, añadirlo a la lista de artículos GC espera de la finalización.
    • Si el objeto de origen ha SuppressFinalize llama en él y una bandera tales se almacena en la cabecera del objeto, unset que en el clon.

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

Me gustaría empezar con algunas citas:

  

De hecho, MemberwiseClone es por lo general mucho mejor que los demás, especialmente para el tipo complejo.

y

  

Estoy confundido. MemberwiseClone () debe aniquilar a la realización de cualquier otra cosa para una copia superficial. [...]

En teoría la mejor ejecución de una copia superficial es un constructor de copia C ++: se sabe el tamaño en tiempo de compilación, y luego hace un clon miembro por miembro de todos los campos. El siguiente mejor cosa está utilizando memcpy o algo similar, que es básicamente la forma en MemberwiseClone debería funcionar. Esto significa que, en teoría, debería destruir todas las otras posibilidades en términos de rendimiento. ¿verdad?

... pero aparentemente no es veloz y no borra todas las otras soluciones. En la parte inferior De hecho, he publicado una solución que es más de 2 veces más rápido. Por lo tanto:. incorrecto

Prueba de las partes internas de MemberwiseClone

Vamos a empezar con un poco de prueba utilizando un tipo simple blittable para comprobar 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 se diseñó de tal manera que podemos comprobar el rendimiento de MemberwiseClone Agaist memcpy prima, lo cual es posible porque se trata de un tipo blittable.

Para probar por sí mismo, compilar con código no seguro, desactivar la supresión JIT, compilar y probar el modo de disparo de distancia. También me he puesto los tiempos después de cada línea que es relevante.

Aplicació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 me corrieron estas pruebas un número de veces, comprueba la salida de montaje para asegurar que la cosa no fue optimizado de distancia, etc. El resultado final es que sé que aproximadamente la cantidad de segundos ésta línea de costos de código, que es 0.40s en mi PC. Esta es nuestra línea de base utilizando 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 te fijas bien en estos números, se dará cuenta de algunas cosas:

  • Creación de un objeto y la copia se tardará más o menos 0.20s. En circunstancias normales, este es el código más rápido posible que puede tener.
  • No obstante, para ello, es necesario precisar y desanclar el objeto. Esto te llevará 0.81 segundos.

¿Por qué es todo esto tan lento?

Mi explicación es que tiene que ver con la GC. Básicamente las implementaciones no pueden confiar en el hecho de que la memoria seguirá siendo el mismo antes y después de una completa GC (La dirección de la memoria se puede cambiar durante un GC, lo que puede suceder en cualquier momento, incluso durante su copia superficial). Esto significa que sólo tiene 2 opciones posibles:

  1. Fijación de los datos y hacer una copia. Tenga en cuenta que GCHandle.Alloc es sólo una de las maneras de hacer esto, es bien sabido que las cosas como C ++ / CLI le dará un mejor rendimiento.
  2. enumeración de los campos. Esto asegurará que entre el GC recoge que no es necesario hacer algo de fantasía, y durante la GC recoge se puede utilizar 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 impacto en el rendimiento debido al procedimiento fijación.

A (mucho) más rápida aplicación

En todos los casos nuestro código no administrado no puede hacer suposiciones sobre el tamaño de los tipos y tiene al pin de datos. Hacer suposiciones sobre el tamaño permite que el compilador para hacer mejor las optimizaciones, como el desenrollado del bucle, asignación de registros, etc (al igual que una copia de C ++ ctor es más rápido que memcpy). No tener que pin de datos significa que no obtenemos un impacto de rendimiento adicional. Desde JIT de .NET para ensamblador, en teoría, esto significa que debemos ser capaces de hacer una implementación más rápida usando sencilla IL emisor, y permitiendo que el compilador para optimizarlo.

Para resumir sobre por qué esto puede ser más rápido que la aplicación nativa?

  1. No requiereel objeto a ser fijado; objetos que se mueven alrededor de las maneja el GC -. y realmente, esto se optimiza sin descanso
  2. Se puede hacer suposiciones sobre el tamaño de la estructura de la copia, y por lo tanto permite una mejor asignación de registros, desenrollado del bucle, etc.

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

Para hacer eso, que básicamente no se puede utilizar más que un call, crear el objeto, y llevar a cabo un montón de instrucciones copy. Se parece un poco a la implementación Cloner anterior, pero algunas diferencias importantes (más significativos: no Dictionary y no hay llamadas 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);
    }
}

He probado 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 es a la par con memcpy, que es más o menos la 'solución óptima en circunstancias normales'.

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

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

Esta es una manera de hacerlo mediante la generación de IL dinámico. He encontrado 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);
    }
}

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

La razón es que:si manual de crear una copia, se debe llamar a uno de los del tipo de constructor, pero el uso de memberwise clon, creo que sólo se copia un bloque de memoria.para esos tipos tiene muy caros de construir acciones, memberwise clon es absolutamente la mejor manera.

Onece escribí este tipo:{string A = Guid.NewGuid().ToString()}, he encontrado memberwise clon es la más rápida de crear una nueva instancia y manual de asignar a los miembros.

El código siguiente resultado:

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

}

por último, doy 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

MemberwiseClone requiere menos mantenimiento. No sé si tener valores de las propiedades por defecto cualquier ayuda, tal vez si podría ignorar elementos con valores por defecto.

Aquí hay una pequeña clase de ayuda que utiliza la reflexión para acceder MemberwiseClone y luego se almacena en caché el delegado evitar el uso de la reflexión más de lo necesario.

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

Se le puede llamar así:

Person b = a.ShallowClone();
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top