Pergunta

Gostaria de saber qual é a maneira mais rápida de fazer rasa copiar em C #? Eu só sei que existem 2 maneiras de fazer cópia superficial:

  1. MemberwiseClone
  2. Copiar cada campo um por um (manual)

Eu achei que (2) é mais rápido que (1). Eu estou querendo saber se há outra maneira de fazer cópia superficial?

Foi útil?

Solução

Este é um assunto complexo, com muitas soluções possíveis e muitos prós e contras de cada um. Há um maravilhoso artigo aqui que descreve várias maneiras diferentes de fazer uma cópia em C #. Para resumir:

  1. Clone manualmente
    Tedioso, mas alto nível de controle.

  2. Clone com MemberwiseClone
    Só cria uma cópia superficial, ou seja, para-tipo de referência campos do objeto original e seu clone se referem ao mesmo objeto.

  3. Clone com reflexão
    cópia superficial por padrão, pode ser re-escrita para fazer cópia profunda. Vantagem: automatizado. Desvantagem:. Reflexão é lento

  4. Clone com serialização
    Fácil, automatizado. Dê-se algum controlo e serialização é mais lento de todos.

  5. Clone com IL, Clone com Métodos de extensão
    soluções mais avançadas, não tão comum.

Outras dicas

Estou confuso. MemberwiseClone() deve aniquilar o desempenho de qualquer outra coisa para uma cópia superficial. Na CLI, qualquer tipo diferente de um RCW deve ser capaz de ser pela seguinte sequência copiado-rasa:

  • Alocar memória no viveiro para o tipo.
  • memcpy os dados a partir do original para o novo. Desde o alvo está no berçário, sem barreiras de escrita são necessários.
  • Se o objeto tem um finalizador definido pelo usuário, adicioná-lo à lista de GC de itens pendentes finalização.
    • Se o objeto de origem tem SuppressFinalize chamado sobre ele e tal bandeira é armazenado no cabeçalho do objeto, unset-lo no clone.

Can alguém da equipe internos CLR explicar por que isso não é o caso?

Eu gostaria de começar com algumas citações:

Na verdade, MemberwiseClone é geralmente muito melhor do que outros, especialmente para o tipo complexo.

e

Estou confuso. MemberwiseClone () deve aniquilar o desempenho de qualquer outra coisa para uma cópia superficial. [...]

Teoricamente a melhor implementação de uma cópia superficial é um construtor de cópia C ++: é sabe o tamanho tempo de compilação, em seguida, faz um clone memberwise de todos os campos. A próxima melhor coisa é usar memcpy ou algo semelhante, que é basicamente como MemberwiseClone deve funcionar. Isso significa que, em teoria, deve destruir todas as outras possibilidades em termos de desempenho. Right?

... mas aparentemente não é super rápido e não destruir todas as outras soluções. No fundo eu realmente postou uma solução que é mais de 2x mais rápido. Assim:. errado

Testing as partes internas do MemberwiseClone

Vamos começar com um pequeno teste usando um tipo blittable simples de verificar os pressupostos subjacentes aqui sobre o desempenho:

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

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

O teste é concebido de maneira tal que podemos verificar o desempenho do MemberwiseClone agaist memcpy cru, que é possível porque este é um tipo blittable.

Para testar por si mesmo, compilar com código inseguro, desative a supressão JIT, modo de versão de compilação e teste de distância. Eu também colocar os horários após cada linha que é relevante.

Implementação 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);

Basicamente eu corri esses testes várias vezes, verificou a saída de montagem para garantir que a coisa não foi otimizado afastado, etc. O resultado final é que eu sei aproximadamente quanto segundos esta linha de custos de código, que é 0.40s no meu PC. Esta é a nossa linha de base usando MemberwiseClone.

Implementação 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 você olhar atentamente para esses números, você vai notar algumas coisas:

  • Criação de um objeto e copiar vai demorar cerca de 0.20s. Em circunstâncias normais, este é o código mais rápido possível você pode ter.
  • No entanto, para fazer isso, você precisa de pinos e desafixar o objeto. Que o levará 0,81 segundos.

Então, por que tudo isso é tão lento?

A minha explicação é que ele tem a ver com o GC. Basicamente as implementações não pode contar com o fato de que a memória vai permanecer o mesmo antes e depois de um GC completo (O endereço da memória pode ser alterada durante a GC, o que pode acontecer a qualquer momento, inclusive durante a sua cópia superficial). Isto significa que você só tem 2 opções possíveis:

  1. Fixando os dados e fazer uma cópia. Note-se que GCHandle.Alloc é apenas uma das maneiras de fazer isso, é sabido que coisas como C ++ / CLI lhe dará um melhor desempenho.
  2. Enumerando os campos. Isto irá assegurar que entre GC recolhe você não precisa fazer nada extravagante, e durante GC recolhe você pode usar a capacidade GC para modificar os endereços na pilha de objetos movidos.

MemberwiseClone vai utilizar o método 1, o que significa que você terá um impacto no desempenho por causa do procedimento fixação.

A (muito) mais rápido implementação

Em todos os casos o nosso código não gerenciado pode não fazer suposições sobre o tamanho dos tipos e tem que dados pin. Fazendo suposições sobre o tamanho permite que o compilador para fazer melhores otimizações, como desdobramento de loop, cadastre-alocação, etc. (tal como uma cópia ctor C ++ é mais rápido que memcpy). Não ter que fixar os meios de dados que não recebem um acerto de desempenho extra. Desde do .NET JIT para montador, em teoria, isto significa que devemos ser capazes de fazer uma implementação mais rápida usando IL simples emissor, e permitindo que o compilador para otimizá-lo.

Assim, para resumir sobre por que isso pode ser mais rápido do que a implementação nativa?

  1. Não requero objecto a ser fixado; objetos que se movem ao redor são tratados pela GC -. e realmente, este é implacavelmente otimizado
  2. Pode fazer suposições sobre o tamanho da estrutura para cópia e, portanto, permite uma melhor alocação de registo, desenrolando loop, etc.

O que estamos buscando é o desempenho de memcpy cru ou melhor:. 0.17s

Para fazer isso, nós basicamente não pode usar mais do que apenas um call, criar o objeto e executar um grupo de instruções copy. Parece um pouco como a implementação Cloner acima, mas algumas diferenças importantes (mais significativos: não Dictionary e nenhuma chamada CreateDelegate redundantes). Aqui vai:

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

Eu testei este código com o resultado: 0.16s. Isto significa que é cerca de 2,5 vezes mais rápido do que MemberwiseClone.

Mais importante ainda, esta velocidade é em-par com memcpy, que é mais ou menos a 'solução óptima sob circunstâncias normais'.

Pessoalmente, acho que esta é a solução mais rápida - ea melhor parte é: se o tempo de execução .NET vai ficar mais rápido (suporte adequado para instruções SSE etc), assim que esta solução.

coisas para quê complicar? MemberwiseClone seria suficiente.

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

Esta é uma maneira de fazê-lo usando geração IL dinâmica. Eu encontrei-o em algum lugar on-line:

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

Na verdade, MemberwiseClone é geralmente muito melhor do que outros, especialmente para o tipo complexo.

A razão é que: se você Manual de criar uma cópia, ele deve chamar um dos construtor do tipo, mas clone uso memberwise, eu acho que basta copiar um bloco de memória. para esses tipos tem ações construto muito caros, clone memberwise é absolutamente o melhor caminho.

Onece i escreveu tal tipo: {String A = Guid.NewGuid (). ToString ()}, Encontrei clone memberwise é muct mais rápido do que criar uma nova instância e membros atribuir manuais.

O código abaixo do resultado:

Copiar Manual: 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;
        }
    }

}

Finalmente, eu forneço o meu código aqui:

    #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 requer menos manutenção. Eu não sei se ter valores de propriedade padrão ajuda qualquer, talvez se podia ignorar itens com valores padrão.

Aqui está uma classe auxiliar pequena que usa reflexão para acesso MemberwiseClone e, em seguida, armazena o delegado para evitar o uso de reflexão mais do que o necessário.

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

Você pode chamá-lo assim:

Person b = a.ShallowClone();
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top