web-dev-qa-db-fra.com

Le moyen le plus rapide de faire une copie superficielle en C #

Je me demande quel est le moyen le plus rapide de faire une copie superficielle en C #? Je sais seulement qu'il y a 2 façons de faire une copie superficielle:

  1. MemberwiseClone
  2. Copiez chaque champ un par un (manuel)

J'ai trouvé que (2) est plus rapide que (1). Je me demande s'il y a une autre façon de faire une copie superficielle?

54
tep

Il s'agit d'un sujet complexe avec de nombreuses solutions possibles et de nombreux avantages et inconvénients pour chacun. Il y a un merveilleux article ici qui décrit plusieurs façons différentes de faire une copie en C #. Résumer:

  1. Cloner manuellement
    Niveau de contrôle fastidieux mais élevé.

  2. Cloner avec MemberwiseClone
    Crée uniquement une copie superficielle, c'est-à-dire que pour les champs de type référence, l'objet d'origine et son clone se réfèrent au même objet.

  3. Clone avec réflexion
    Copie peu profonde par défaut, peut être réécrite pour faire une copie complète. Avantage: automatisé. Inconvénient: la réflexion est lente.

  4. Clone avec sérialisation
    Facile, automatisé. Abandonnez un certain contrôle et la sérialisation est la plus lente de toutes.

  5. Cloner avec IL, Cloner avec des méthodes d'extension
    Solutions plus avancées, moins courantes.

72
Nick Stamas

Je suis confus. MemberwiseClone() devrait annihiler les performances de tout le reste pour une copie superficielle. Dans l'interface CLI, tout type autre qu'un RCW doit pouvoir être copié de manière superficielle par la séquence suivante:

  • Allouez de la mémoire dans la pépinière pour le type.
  • memcpy les données de l'original au nouveau. Étant donné que la cible se trouve dans la pépinière, aucune barrière d'écriture n'est requise.
  • Si l'objet a un finaliseur défini par l'utilisateur, ajoutez-le à la liste GC des éléments en attente de finalisation.
    • Si l'objet source est appelé SuppressFinalize et qu'un tel indicateur est stocké dans l'en-tête de l'objet, désactivez-le dans le clone.

Un membre de l'équipe interne du CLR peut-il expliquer pourquoi ce n'est pas le cas?

28
Sam Harwell

J'aimerais commencer par quelques citations:

En fait, MemberwiseClone est généralement bien meilleur que les autres, en particulier pour les types complexes.

et

Je suis confus. MemberwiseClone () devrait annihiler les performances de toute autre chose pour une copie superficielle. [...]

Théoriquement, la meilleure implémentation d'une copie superficielle est un constructeur de copie C++: il connaît la taille au moment de la compilation, puis fait un clone membre de tous les champs. La prochaine meilleure chose consiste à utiliser memcpy ou quelque chose de similaire, qui est essentiellement la façon dont MemberwiseClone devrait fonctionner. Cela signifie, en théorie, qu'il devrait effacer toutes les autres possibilités en termes de performances. N'est-ce pas?

... mais apparemment, ce n'est pas très rapide et cela n'efface pas toutes les autres solutions. En bas, j'ai en fait publié une solution qui est plus de 2x plus rapide. Donc: Mauvais.

Test des internes de MemberwiseClone

Commençons par un petit test en utilisant un simple type blittable pour vérifier les hypothèses sous-jacentes ici sur les performances:

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

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

Le test est conçu de telle manière que nous pouvons vérifier les performances de MemberwiseClone agaist raw memcpy, ce qui est possible car il s'agit d'un type blittable.

Pour tester par vous-même, compilez avec du code dangereux, désactivez la suppression JIT, compilez le mode de libération et testez-le. J'ai également mis les horaires après chaque ligne pertinente.

Implémentation 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);

Fondamentalement, j'ai exécuté ces tests un certain nombre de fois, vérifié la sortie de l'assembly pour m'assurer que la chose n'était pas optimisée, etc. Le résultat final est que je sais environ combien de secondes cette ligne de code coûte, ce qui est de 0,40 s mon ordinateur. Il s'agit de notre base de référence utilisant MemberwiseClone.

Implémentation 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 vous regardez attentivement ces chiffres, vous remarquerez quelques choses:

  • Créer un objet et le copier prendra environ 0,20 s. Dans des circonstances normales, il s'agit du code le plus rapide possible.
  • Cependant, pour ce faire, vous devez épingler et détacher l'objet. Cela vous prendra 0,81 seconde.

Alors pourquoi tout cela est-il si lent?

Mon explication est que cela a à voir avec le GC. Fondamentalement, les implémentations ne peuvent pas s'appuyer sur le fait que la mémoire restera la même avant et après un GC complet (l'adresse de la mémoire peut être modifiée pendant un GC, ce qui peut arriver à tout moment, y compris lors de votre copie superficielle). Cela signifie que vous n'avez que 2 options possibles:

  1. Épingler les données et en faire une copie. Notez que GCHandle.Alloc n'est qu'une des façons de le faire, il est bien connu que des choses comme C++/CLI vous donneront de meilleures performances.
  2. Énumération des champs. Cela garantira qu'entre les collectes GC, vous n'avez rien à faire de fantaisie, et pendant les collectes GC, vous pouvez utiliser la capacité GC pour modifier les adresses sur la pile d'objets déplacés.

MemberwiseClone utilisera la méthode 1, ce qui signifie que vous obtiendrez un résultat de performance en raison de la procédure d'épinglage.

ne implémentation (beaucoup) plus rapide

Dans tous les cas, notre code non managé ne peut pas faire d'hypothèses sur la taille des types et il doit épingler les données. Faire des hypothèses sur la taille permet au compilateur de faire de meilleures optimisations, comme le déroulement de boucle, l'allocation de registre, etc. (tout comme un ctor de copie C++ est plus rapide que memcpy). Ne pas avoir à épingler de données signifie que nous n'obtenons pas de performances supplémentaires. Depuis .NET JIT vers assembleur, cela signifie en théorie que nous devrions être en mesure de faire une implémentation plus rapide en utilisant une simple émission d'IL et en permettant au compilateur de l'optimiser.

Donc, pour résumer pourquoi cela peut être plus rapide que l'implémentation native?

  1. Il ne nécessite pas que l'objet soit épinglé; les objets qui se déplacent sont gérés par le GC - et vraiment, cela est optimisé sans relâche.
  2. Il peut faire des hypothèses sur la taille de la structure à copier, et permet donc une meilleure allocation de registre, un déroulement de boucle, etc.

Ce que nous visons, c'est la performance de memcpy brut ou mieux: 0,17s.

Pour ce faire, nous ne pouvons pas utiliser plus qu'un simple call, créer l'objet et exécuter un tas d'instructions copy. Cela ressemble un peu à l'implémentation de Cloner ci-dessus, mais quelques différences importantes (plus importantes: pas d'appels Dictionary et pas d'appels CreateDelegate redondants). Voici:

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

J'ai testé ce code avec le résultat: 0,16 s. Cela signifie qu'il est environ 2,5 fois plus rapide que MemberwiseClone.

Plus important encore, cette vitesse est comparable à memcpy, qui est plus ou moins la "solution optimale dans des circonstances normales".

Personnellement, je pense que c'est la solution la plus rapide - et la meilleure partie est: si le runtime .NET deviendra plus rapide (prise en charge appropriée de SSE, etc.), cette solution aussi.

27
atlaste

Pourquoi compliquer les choses? MemberwiseClone suffirait.

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

C'est une façon de le faire en utilisant la génération dynamique d'IL. Je l'ai trouvé quelque part en ligne:

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

En fait, MemberwiseClone est généralement bien meilleur que les autres, en particulier pour les types complexes.

La raison en est que: si vous créez manuellement une copie, il doit appeler l'un des constructeurs du type, mais utiliser un clone membre, je suppose qu'il suffit de copier un bloc de mémoire. pour ces types a des actions de construction très coûteuses, le clone membre est absolument le meilleur moyen.

Une fois que j'ai écrit un tel type: {string A = Guid.NewGuid (). ToString ()}, j'ai trouvé que le clone par membre est plus rapide que de créer une nouvelle instance et d'assigner manuellement des membres.

Le résultat du code ci-dessous:

Copie manuelle: 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;
        }
    }

}

enfin, je fournis mon code ici:

    #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
5
dexiang

MemberwiseClone nécessite moins de maintenance. Je ne sais pas si avoir des valeurs de propriété par défaut aide à tout, peut-être si pourrait ignorer les éléments avec des valeurs par défaut.

4
Eric Schneider

Voici une petite classe d'assistance qui utilise la réflexion pour accéder à MemberwiseClone puis met en cache le délégué pour éviter d'utiliser la réflexion plus que nécessaire.

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

Vous pouvez l'appeler comme ceci:

Person b = a.ShallowClone();
1
Tim Pohlmann