Самый быстрый способ сделать мелкую копию в С#
Интересно, что самый быстрый способ сделать мелкое копирование на С#? Я знаю только, что есть 2 способа сделать мелкую копию:
- MemberwiseClone
- Скопировать каждое поле по одному (вручную)
Я обнаружил, что (2) быстрее, чем (1). Мне интересно, есть ли другой способ сделать мелкое копирование?
Ответы
Ответ 1
Это сложная тема с множеством возможных решений и множеством плюсов и минусов для каждого. Здесь есть замечательная статья here, в которой описываются несколько различных способов сделать копию в С#. Подводя итог:
Клонировать вручную
Утомительный, но высокий уровень контроля.
Клон с MemberwiseClone
Создает только поверхностную копию, то есть для полей ссылочного типа исходный объект и его клон ссылаются на один и тот же объект.
Клон с отражением
По умолчанию мелкая копия, может быть переписана, чтобы сделать глубокую копию. Преимущество: автоматизировано. Недостаток: медленное отражение.
Клон с сериализацией
Легко, автоматизировано. Откажитесь от контроля, и сериализация будет самой медленной из всех.
Клон с ИЛ, клон с методами расширения
Более продвинутые решения, не такие распространенные.
Ответ 2
Я в замешательстве. MemberwiseClone()
должен аннулировать производительность чего-либо еще для мелкой копии. В CLI любой тип, отличный от RCW, должен быть неглубоко скопирован в следующей последовательности:
- Выделите память в детской для типа.
-
memcpy
данные от оригинала к новому. Поскольку цель находится в питомнике, не требуется никаких барьеров для записи.
- Если объект имеет определяемый пользователем финализатор, добавьте его в список GC элементов, ожидающих завершения.
- Если исходный объект имеет
SuppressFinalize
, вызывающий его, и такой флаг сохраняется в заголовке объекта, отключите его в клоне.
Может ли кто-нибудь из команды внутренних разработчиков CLR объяснить, почему это не так?
Ответ 3
Я хотел бы начать с нескольких цитат:
На самом деле 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 возможных варианта:
- Закрепление данных и копирование. Обратите внимание, что
GCHandle.Alloc
- это только один из способов сделать это, хорошо известно, что такие вещи, как C++/CLI, обеспечат вам лучшую производительность.
- Перечисление полей. Это гарантирует, что между сборками GC вам не нужно делать что-то необычное, а во время сборов GC вы можете использовать возможность GC для изменения адресов в стеке перемещенных объектов.
MemberwiseClone
будет использовать метод 1, что означает снижение производительности из-за процедуры закрепления.
(Намного) более быстрая реализация
Во всех случаях наш неуправляемый код не может делать предположения о размере типов, и он должен закреплять данные. Создание предположений о размере позволяет компилятору оптимизировать работу, например, развертывание цикла, распределение регистров и т.д. (Точно так же, как копирование C++ быстрее, чем memcpy
). Отсутствие необходимости прикреплять данные означает, что мы не получим дополнительного снижения производительности. Начиная с .NET JIT до ассемблера, теоретически это означает, что мы должны быть в состоянии сделать более быструю реализацию с использованием простого IL-излучения и позволить компилятору оптимизировать его.
Итак, подведем итог, почему это может быть быстрее, чем собственная реализация?
- Это не требует, чтобы объект был закреплен; объекты, которые перемещаются, обрабатываются GC - и это действительно постоянно оптимизируется.
- Он может делать предположения о размере копируемой структуры и, следовательно, позволяет лучше распределять регистры, развертывать циклы и т.д.
То, к чему мы стремимся, - это производительность 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 и т.д.), То же самое будет и с этим решением.
Примечание редакции:
В приведенном выше примере кода предполагается, что конструктор по умолчанию является открытым. Если это не так, вызов GetConstructor
возвращает ноль. В этом случае используйте одну из других подписей GetConstructor
для получения защищенных или частных конструкторов.
Видеть https://docs.microsoft.com/en-us/dotnet/api/system.type.getconstructor?view=netframework-4.8
Ответ 4
Зачем усложнять вещи? MemberwiseClone будет достаточно.
public class ClassA : ICloneable
{
public object Clone()
{
return this.MemberwiseClone();
}
}
// let 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();
}
Ответ 5
Это способ сделать это с использованием динамического генерации 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);
}
}
Ответ 6
Фактически, 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
Ответ 7
MemberwiseClone требует меньше обслуживания. Я не знаю, помогает ли значение свойства по умолчанию любому, возможно, если бы он мог игнорировать элементы со значениями по умолчанию.
Ответ 8
Вот небольшой вспомогательный класс, который использует отражение для доступа к 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();