Самый быстрый способ сделать мелкое копирование в C # - PullRequest
53 голосов
/ 08 июня 2009

Интересно, какой самый быстрый способ сделать поверхностное копирование в C #? Я только знаю, что есть 2 способа сделать поверхностное копирование:

  1. MemberwiseClone
  2. Копировать каждое поле одно за другим (вручную)

Я обнаружил, что (2) быстрее, чем (1). Мне интересно, есть ли другой способ сделать поверхностное копирование?

Ответы [ 8 ]

71 голосов
/ 08 июня 2009

Это сложный предмет с множеством возможных решений и множеством плюсов и минусов для каждого. Здесь есть замечательная статья , в которой описаны несколько различных способов сделать копию в C #. Подведем итог:

  1. Клонировать вручную
    Утомительный, но высокий уровень контроля.

  2. Клон с MemberwiseClone
    Создает только поверхностную копию, то есть для полей ссылочного типа исходный объект и его клон ссылаются на один и тот же объект.

  3. Клон с отражением
    По умолчанию мелкая копия, может быть переписана, чтобы сделать глубокую копию. Преимущество: автоматизировано. Недостаток: медленное отражение.

  4. Клон с сериализацией
    Легко, автоматизировано. Откажитесь от контроля и сериализация будет самой медленной из всех.

  5. Клон с IL, клон с методами расширения
    Более продвинутые решения, не такие распространенные.

28 голосов
/ 21 августа 2009

Я в замешательстве. MemberwiseClone() должно уничтожить выполнение чего-либо еще для мелкой копии. В CLI любой тип, кроме RCW, должен иметь возможность мелкого копирования в следующей последовательности:

  • Выделите память в детской для типа.
  • memcpy данные от оригинала к новому. Поскольку цель находится в детской, никаких барьеров для записи не требуется.
  • Если объект имеет определенный пользователем финализатор, добавьте его в список элементов GC, ожидающих завершения.
    • Если исходному объекту был вызван SuppressFinalize и такой флаг хранится в заголовке объекта, сбросьте его в клоне.

Может ли кто-нибудь из команды внутреннего персонала CLR объяснить, почему это не так?

24 голосов
/ 20 июля 2015

Я хотел бы начать с нескольких цитат:

На самом деле 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 agaist raw 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 возможных варианта:

  1. Закрепление данных и копирование. Обратите внимание, что GCHandle.Alloc - это только один из способов сделать это, хорошо известно, что такие вещи, как C ++ / CLI, обеспечат вам лучшую производительность.
  2. Перечисление полей. Это гарантирует, что между сборками GC вам не нужно делать что-то необычное, а во время сборов GC вы можете использовать возможность GC для изменения адресов в стеке перемещенных объектов.

MemberwiseClone будет использовать метод 1, что означает снижение производительности из-за процедуры закрепления.

(намного) более быстрая реализация

Во всех случаях наш неуправляемый код не может делать предположения о размере типов, и он должен закреплять данные. Делая предположения о размере, компилятор может оптимизировать работу, например, развертывание цикла, распределение регистров и т. Д. (Точно так же, как копирование C ++ быстрее, чем memcpy). Отсутствие необходимости прикреплять данные означает, что мы не получим дополнительного снижения производительности. Поскольку .NET JIT для ассемблера, теоретически это означает, что мы должны быть в состоянии сделать более быструю реализацию, используя простое излучение IL и позволяя компилятору оптимизировать его.

Итак, подведем итог, почему это может быть быстрее, чем собственная реализация?

  1. Не требует закрепления объекта; объекты, которые перемещаются, обрабатываются GC - и действительно, это неуклонно оптимизируется.
  2. Он может делать предположения о размере копируемой структуры и, следовательно, позволяет лучше распределять регистры, развертывать циклы и т. Д.

То, к чему мы стремимся, - это производительность raw 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 и т. Д.), То и это решение будет таким же.

13 голосов
/ 08 марта 2013

Зачем все усложнять? 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();
}
8 голосов
/ 08 июня 2009

Это способ сделать это с помощью динамической генерации 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);
    }
}
5 голосов
/ 19 февраля 2014

На самом деле MemberwiseClone обычно намного лучше других, особенно для сложных типов.

Причина в том, что: если вы вручную создаете копию, она должна вызывать один из конструкторов типа, но использовать клонирование по элементам, я думаю, это просто скопировать блок памяти. для тех типов есть очень дорогие конструктивные действия, членство в клоне является абсолютно лучшим способом.

Раз я написал такой тип: {string A = Guid.NewGuid (). ToString ()}, я обнаружил, что для каждого элемента клон работает быстрее, чем создание нового экземпляра и назначение членов вручную.

Результат с кодом ниже:

Ручная копия: 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;
        }
    }

}

наконец, я предоставляю свой код здесь:

    #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
4 голосов
/ 08 июня 2009

MemberwiseClone требует меньше обслуживания. Я не знаю, помогает ли какое-либо значение свойств по умолчанию, может быть, если можно игнорировать элементы со значениями по умолчанию.

1 голос
/ 24 октября 2018

Вот небольшой вспомогательный класс, который использует отражение для доступа к 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();
...