2011-04-13 14 views
10

Sé que la serialización de un objeto es (que yo sepa) la única manera de copiar profundamente un objeto (siempre que no sea con estado como IO y otras cosas), pero ¿es una forma particularmente más eficiente que otra?¿Cuál es la forma más eficiente de copiar en profundidad un objeto en Ruby?

Por ejemplo, dado que estoy usando Rails, siempre pude usar ActiveSupport::JSON, to_xml - y por lo que puedo decir, la clasificación del objeto es una de las formas más aceptadas de hacerlo. Esperaría que el marshalling sea probablemente el más eficiente de estos ya que es un interno de Ruby, pero ¿me falta algo?

Editar: tenga en cuenta que su aplicación es algo que ya he cubierto - No quiero para reemplazar los métodos de copia de poca profundidad (como dup y clone) existente, por lo que sólo terminan añadiendo probable Object::deep_copy, el resultado de los cuales es cualquiera de los métodos anteriores (o cualquier sugerencia que tenga :) que tenga la menor sobrecarga.

Respuesta

21

Me preguntaba lo mismo, por lo que comparé algunas técnicas diferentes entre sí. Me preocupaba principalmente Arrays y Hashes: no probé ningún objeto complejo. Tal vez, como era de esperar, una implementación personalizada de clonación profunda demostró ser la más rápida. Si está buscando una implementación rápida y fácil, Marshal parece ser el camino a seguir.

También comparé una solución XML con Rails 3.0.7, que no se muestra a continuación. Fue mucho, mucho más lento, ~ 10 segundos para solo 1000 iteraciones (las soluciones debajo de todo funcionaron 10,000 veces para el punto de referencia).

Dos notas sobre mi solución JSON. Primero, utilicé la variante C, versión 1.4.3. En segundo lugar, en realidad no funciona al 100%, ya que los símbolos se convertirán en cadenas.

Todo esto se ejecutó con ruby ​​1.9.2p180.

#!/usr/bin/env ruby 
require 'benchmark' 
require 'yaml' 
require 'json/ext' 
require 'msgpack' 

def dc1(value) 
    Marshal.load(Marshal.dump(value)) 
end 

def dc2(value) 
    YAML.load(YAML.dump(value)) 
end 

def dc3(value) 
    JSON.load(JSON.dump(value)) 
end 

def dc4(value) 
    if value.is_a?(Hash) 
    result = value.clone 
    value.each{|k, v| result[k] = dc4(v)} 
    result 
    elsif value.is_a?(Array) 
    result = value.clone 
    result.clear 
    value.each{|v| result << dc4(v)} 
    result 
    else 
    value 
    end 
end 

def dc5(value) 
    MessagePack.unpack(value.to_msgpack) 
end 

value = {'a' => {:x => [1, [nil, 'b'], {'a' => 1}]}, 'b' => ['z']} 

Benchmark.bm do |x| 
    iterations = 10000 
    x.report {iterations.times {dc1(value)}} 
    x.report {iterations.times {dc2(value)}} 
    x.report {iterations.times {dc3(value)}} 
    x.report {iterations.times {dc4(value)}} 
    x.report {iterations.times {dc5(value)}} 
end 

resultados en:

user  system  total  real 
0.230000 0.000000 0.230000 ( 0.239257) (Marshal) 
3.240000 0.030000 3.270000 ( 3.262255) (YAML) 
0.590000 0.010000 0.600000 ( 0.601693) (JSON) 
0.060000 0.000000 0.060000 ( 0.067661) (Custom) 
0.090000 0.010000 0.100000 ( 0.097705) (MessagePack) 
+0

¡Gracias, Evan! Buenas cosas, aprecio los puntos de referencia. :) – mway

+1

Hola @Evan Pon, agregué [MessagePack] (http://msgpack.org/) en tus ejemplos. Es una buena opción. –

+0

MessagePack se ve muy rápido (2 veces más rápido que Custom en mi máquina). ¿Podría actualizar la respuesta con la recomendación de usarla en lugar de Marshal? –

1

Creo que debe agregar un método initialize_copy a la clase que está copiando. Luego ponga la lógica para la copia profunda allí. Luego, cuando llames a clonar, disparará ese método. No lo he hecho, pero eso es lo que entiendo.

Creo que el plan B sería simplemente reemplazando el método clone: ​​

class CopyMe 
    attr_accessor :var 
    def initialize var='' 
     @var = var 
    end  
    def clone deep= false 
     deep ? CopyMe.new(@var.clone) : CopyMe.new() 
    end 
end 

a = CopyMe.new("test") 
puts "A: #{a.var}" 
b = a.clone 
puts "B: #{b.var}" 
c = a.clone(true) 
puts "C: #{c.var}" 

salida

[email protected]:~/projects$ ruby ~/Desktop/clone.rb 
A: test 
B: 
C: test 

Estoy seguro de que podría hacer que el refrigerador con un poco de bricolaje, pero para bien o para mal esa es probablemente la forma en que lo haría.

+0

Apreciar los comentarios: esa es una forma de reemplazarlo, pero, si bien termina siendo implementado, idealmente sería discreto y dejaría los métodos originales en una copia superficial intacta (por ejemplo, solo agregaría 'Object :: deep_copy') ¿Has visto algo sobre qué método ofrece la menor sobrecarga? – mway

+0

Actualizado. Espero que eso ayude un poco. – mikewilliamson

+0

+1, vea un ejemplo aquí: http://blog.rubybestpractices.com/posts/rklemme/018-Complete_Class.html#Cloning – akostadinov

0

Probablemente la razón de Ruby no contiene un clon de profundidad tiene que ver con la complejidad del problema. Vea las notas al final.

Para hacer una copia que "copie en profundidad", Hashes, Arrays y valores elementales, es decir, Hacer una copia de cada elemento en el original de tal forma que la copia tendrá los mismos valores, pero los nuevos objetos, puede utilizar esto:

class Object 
    def deepclone 
    case 
    when self.class==Hash 
     hash = {} 
     self.each { |k,v| hash[k] = v.deepclone } 
     hash 
    when self.class==Array 
     array = [] 
     self.each { |v| array << v.deepclone } 
     array 
    else 
     if defined?(self.class.new) 
     self.class.new(self) 
     else 
     self 
     end 
    end 
    end 
end 

Si desea volver a definir el comportamiento del método de Ruby clone, puede nómbrelo solo clone en lugar de deepclone (en 3 lugares), pero no tengo idea de cómo la redefinición del comportamiento del clon de Ruby afectará a las bibliotecas Ruby, o Ruby on Rails, por lo que Caveat Emptor. Personalmente, no puedo recomendar hacer eso.

Por ejemplo:

a = {'a'=>'x','b'=>'y'}       => {"a"=>"x", "b"=>"y"} 
b = a.deepclone         => {"a"=>"x", "b"=>"y"} 
puts "#{a['a'].object_id}/#{b['a'].object_id}" => 15227640/15209520 

Si desea sus clases a deepclone correctamente, su método new (inicializar) debe ser capaz de deepclone un objeto de esa clase en la forma estándar, es decir, si el se da el primer parámetro, se supone que es un objeto a ser clonado profundo.

Supongamos que queremos una clase M, por ejemplo. El primer parámetro debe ser un objeto opcional de la clase M. Aquí tenemos un segundo argumento opcional z para preestablecer el valor de z en el nuevo objeto.

class M 
    attr_accessor :z 
    def initialize(m=nil, z=nil) 
    if m 
     # deepclone all the variables in m to the new object 
     @z = m.z.deepclone 
    else 
     # default all the variables in M 
     @z = z # default is nil if not specified 
    end 
    end 
end 

El z preestablecido se ignora durante la clonación de aquí, pero el método puede tener un comportamiento diferente. Los objetos de esta clase serían creados de esta manera:

# a new 'plain vanilla' object of M 
m=M.new          => #<M:0x0000000213fd88 @z=nil> 
# a new object of M with m.z pre-set to 'g' 
m=M.new(nil,'g')        => #<M:0x00000002134ca8 @z="g"> 
# a deepclone of m in which the strings are the same value, but different objects 
n=m.deepclone         => #<M:0x00000002131d00 @z="g"> 
puts "#{m.z.object_id}/#{n.z.object_id}" => 17409660/17403500 

donde los objetos de la clase M son parte de una matriz:

a = {'a'=>M.new(nil,'g'),'b'=>'y'}    => {"a"=>#<M:0x00000001f8bf78 @z="g">, "b"=>"y"} 
b = a.deepclone         => {"a"=>#<M:0x00000001766f28 @z="g">, "b"=>"y"} 
puts "#{a['a'].object_id}/#{b['a'].object_id}" => 12303600/12269460 
puts "#{a['b'].object_id}/#{b['b'].object_id}" => 16811400/17802280 

Notas:

  • If deepclone intentos de clonar un objeto que no se clona a sí mismo de la manera estándar, puede fallar.
  • Si deepclone intenta clonar un objeto que puede clonarse a sí mismo de la manera estándar, y si se trata de una estructura compleja, puede (y probablemente lo hará) crear un clon superficial de sí mismo.
  • deepclone no copia profundamente las claves en Hashes. La razón es que generalmente no se tratan como datos, pero si cambia hash[k] a hash[k.deepclone], también se copiarán profundamente.
  • Ciertos valores elementales no tienen el método new, como Fixnum. Estos objetos siempre tienen la misma ID de objeto, y se copian, no se clonan.
  • Tenga cuidado porque cuando copie profundamente, dos partes de su Hash o matriz que contengan el mismo objeto en el original contendrán diferentes objetos en el deepclone.
Cuestiones relacionadas