Самый быстрый способ выполнить мелкое копирование на C#

StackOverflow https://stackoverflow.com/questions/966451

  •  12-09-2019
  •  | 
  •  

Вопрос

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

  1. MemberwiseClone
  2. Скопируйте каждое поле по одному (вручную)

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

Это было полезно?

Решение

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

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

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

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

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

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

Другие советы

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

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

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

Хочу начать с нескольких цитат:

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

и

Я в замешательстве.MemberwiseClone() должен уничтожить производительность всего остального для мелкой копии.[...]

Теоретически лучшей реализацией поверхностного копирования является конструктор копирования C++:это знает размер во время компиляции, а затем выполняет почленное клонирование всех полей.Следующее лучшее - использовать memcpy или что-то подобное, и именно так MemberwiseClone должно сработать.Это означает, что теоретически это должно стереть все другие возможности с точки зрения производительности. Верно?

...но, видимо, он не работает быстро и не уничтожает все другие решения.Внизу я фактически опубликовал решение, которое работает более чем в 2 раза быстрее.Так: Неправильный.

Тестирование внутреннего устройства MemberwiseClone

Давайте начнем с небольшого теста с использованием простого преобразуемого типа, чтобы проверить основные предположения о производительности:

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

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

Тест устроен таким образом, что мы можем проверить работоспособность MemberwiseClone агаист сырой memcpy, что возможно, поскольку это преобразуемый тип.

Чтобы протестировать самостоятельно, скомпилируйте небезопасный код, отключите 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. Для этого не требуется, чтобы объект был закреплен;объекты, которые перемещаются, обрабатываются сборщиком мусора — и на самом деле это постоянно оптимизируется.
  2. Он может делать предположения о размере копируемой структуры и, следовательно, позволяет лучше распределять регистры, разворачивать циклы и т. д.

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

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

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

На самом деле, 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

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

Вот небольшой вспомогательный класс, который использует отражение для доступа. 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();
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top