Domanda

Mi chiedo qual è il modo più veloce per eseguire copie superficiali in C#?So solo che ci sono 2 modi per eseguire una copia superficiale:

  1. MemberwiseClone
  2. Copia ogni campo uno per uno (manuale)

Ho scoperto che (2) è più veloce di (1).Mi chiedo se esiste un altro modo per eseguire copie superficiali?

È stato utile?

Soluzione

Questo è un argomento complesso con un sacco di possibili soluzioni e molti pro e contro di ciascuno. C'è un bellissimo articolo qui che delinea diversi modi di fare una copia di C #. Per riassumere:


  1. Clone manualmente livello di noioso, ma alta di controllo.

  2. Il clone con MemberwiseClone
    crea solo una copia, vale a dire per riferimento di tipo campi l'oggetto originale e il suo clone si riferiscono allo stesso oggetto.

  3. Clone con la riflessione
    copia superficiale per impostazione predefinita, può essere riscritta per fare copia completa. Vantaggio: automatizzato. Svantaggio:. Riflessione è lento

  4. Il clone con serializzazione
    Facile, automatizzato. Dare un po 'di controllo e la serializzazione è più lento di tutti.

  5. Clone con IL, clone con Metodi di estensione
    soluzioni più avanzate, non così comune.

Altri suggerimenti

Sono confuso. MemberwiseClone() dovrebbe annientare le prestazioni di qualsiasi altra cosa per una copia. Nella CLI, qualsiasi tipo diverso da un RCW dovrebbe essere in grado di essere superficiale copiato dalla seguente sequenza:

  • allocare memoria in vivaio per il tipo.
  • memcpy i dati dal originale al nuovo. Dal momento che l'obiettivo è nella stanza dei bambini, non sono necessarie barriere di scrittura.
  • Se l'oggetto ha un finalizzatore definito dall'utente, aggiungerlo alla lista GC di elementi in attesa di finalizzazione.
    • Se l'oggetto di origine è SuppressFinalize chiamato su di esso e un tale flag è memorizzata nell'intestazione oggetto, non impostato nel clone.

qualcuno del team interni CLR può spiegare il motivo per cui questo non è il caso?

Mi piacerebbe iniziare con alcune citazioni:

  

In realtà, MemberwiseClone di solito è molto meglio di altri, in particolare per il tipo complesso.

e

  

Sono confuso. MemberwiseClone () dovrebbe annientare le prestazioni di qualsiasi altra cosa per una copia. [...]

In teoria la migliore implementazione di una copia è un C ++ costruttore di copia: è sa le dimensioni in fase di compilazione, e poi fa un clone membro a membro di tutti i campi. La cosa migliore sta usando memcpy o qualcosa di simile, che è fondamentalmente come MemberwiseClone dovrebbe funzionare. Ciò significa, in teoria dovrebbe cancellare tutte le altre possibilità in termini di prestazioni. Giusto?

... ma a quanto pare non è velocissimo e non obliterare tutte le altre soluzioni. In fondo in realtà ho postato una soluzione che è oltre 2 volte più veloce. Quindi:. sbagliato

Verifica la struttura interna di MemberwiseClone

Cominciamo con un piccolo test utilizzando un tipo semplice copiabili per verificare le ipotesi sottostanti qui sulle prestazioni:

[StructLayout(LayoutKind.Sequential)]
public class ShallowCloneTest
{
    public int Foo;
    public long Bar;

    public ShallowCloneTest Clone()
    {
        return (ShallowCloneTest)base.MemberwiseClone();
    }
}

Il test è concepito in modo tale da poter controllare le prestazioni del MemberwiseClone agaist memcpy grezzo, che è possibile perché questo è un tipo copiabili.

Per provare da soli, compilare con il codice non sicuro, disattivare la soppressione JIT, compilare modalità di rilascio e testare via. Ho anche messo i tempi dopo ogni linea che è rilevante.

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

Fondamentalmente corse queste prove un numero di volte, controllato l'uscita dell'assemblaggio per assicurarsi che la cosa non è stato ottimizzato distanza, ecc Il risultato finale è che so approssimativamente quanto secondi questa linea di costi di codice, che è 0.40s sul mio PC. Questa è la nostra linea di base utilizzando MemberwiseClone.

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

Se si guarda da vicino a questi numeri, si noterà un paio di cose:

  • La creazione di un oggetto e la copia ci vorrà circa 0.20s. In circostanze normali questo è il codice più veloce possibile si può avere.
  • Tuttavia, per fare questo, è necessario al pin e sbloccare l'oggetto. Che vi porterà 0,81 secondi.

Allora, perché è tutto questo è così lento?

La mia spiegazione è che ha a che fare con il GC. In sostanza le implementazioni non possono fare affidamento sul fatto che la memoria rimarrà la stessa prima e dopo un GC completo (L'indirizzo della memoria può essere cambiato nel corso di un GC, che può accadere in qualsiasi momento, anche durante la vostra copia superficiale). Questo significa che solo 2 possibili opzioni:

  1. Pinning i dati e fare una copia. Si noti che GCHandle.Alloc è solo uno dei modi per fare questo, è ben noto che le cose come C ++ / CLI vi darà prestazioni migliori.
  2. L'enumerazione dei campi. Questo farà sì che tra GC raccoglie non è necessario fare niente di eccezionale, e durante la GC raccoglie è possibile utilizzare la capacità GC per modificare gli indirizzi sulla pila di oggetti spostati.

MemberwiseClone userà il metodo 1, il che significa che si otterrà un calo di prestazioni a causa della procedura di pinning.

A (molto) più veloce implementazione

In tutti i casi il nostro codice non gestito non può fare ipotesi circa le dimensioni dei tipi e deve appuntare i dati. Fare ipotesi sulla dimensione consente al compilatore di fare ottimizzazioni migliori, come il ciclo di srotolamento, allocazione dei registri, ecc (proprio come un C ++ copia ctor è più veloce di memcpy). Non dover appuntare i dati significa che non si ottiene un successo in più di prestazioni. Dal momento che .NET di JIT per Assembler, in teoria questo significa che dovremmo essere in grado di fare una più rapida attuazione utilizzando semplici IL emettono, e consentendo al compilatore di ottimizzarlo.

Quindi, per riassumere il motivo per cui questo può essere più veloce rispetto alla implementazione nativa?

  1. Non richiedel'oggetto da bloccato; oggetti che si muovono intorno sono gestiti dal GC -. e davvero, questo è senza sosta ottimizzati
  2. Si può fare ipotesi circa la dimensione della struttura da copiare, e quindi permette una migliore allocazione dei registri, svolgimento del ciclo, ecc.

Quello che stiamo puntando è la performance del memcpy crudo o meglio:. 0.17s

Per fare questo, abbiamo praticamente non possiamo usare più di un semplice call, creare l'oggetto, ed eseguire una serie di istruzioni copy. Sembra un po 'come l'attuazione Cloner sopra, ma alcune importanti differenze (più significative: no Dictionary e nessuna chiamata CreateDelegate ridondanti). Qui 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);
    }
}

Ho provato questo codice con il risultato: 0.16s. Questo significa che è circa 2,5 volte più veloce di MemberwiseClone.

Ancora più importante, questa velocità è in pari con memcpy, che è più o meno la 'soluzione ottimale in circostanze normali'.

Personalmente, penso che questa sia la soluzione più veloce - e la parte migliore è: se il runtime .NET sarà più veloce (il supporto adeguato per SSE istruzioni, ecc), così sarà questa soluzione.

Perché complicare le cose? MemberwiseClone sarebbe sufficiente.

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

Questo è un modo per farlo utilizzando generazione IL dinamica. L'ho trovato da qualche parte in linea:

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

In effetti, MemberwiseClone è solitamente molto migliore di altri, soprattutto per i tipi complessi.

Il motivo è questo: se crei manualmente una copia, deve chiamare uno dei costruttori del tipo, ma usa il clone membro, immagino che copi semplicemente un blocco di memoria.per questi tipi ha azioni di costruzione molto costose, il clone a membro è assolutamente il modo migliore.

Una volta ho scritto questo tipo:{string A = Guid.NewGuid().ToString()}, ho scoperto che il clone membro è molto più veloce rispetto alla creazione di una nuova istanza e all'assegnazione manuale dei membri.

Il risultato del codice seguente:

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

}

infine, fornisco il mio codice qui:

    #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 richiede meno manutenzione. Non so se avere i valori delle proprietà di default aiuta qualsiasi, forse se poteva ignorare gli elementi con i valori di default.

Ecco una piccola classe di supporto che utilizza la riflessione per accedere MemberwiseClone e quindi memorizza nella cache il delegato di evitare l'uso di riflessione più del necessario.

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

Si può chiamare in questo modo:

Person b = a.ShallowClone();
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top