¿Por qué se prefiere el operador de pala (<<) sobre más-igual (+ =) al construir una cadena en Ruby?

156

Estoy trabajando a través de Ruby Koans.

El test_the_shovel_operator_modifies_the_original_stringKoan en about_strings.rb incluye el siguiente comentario:

Los programadores de Ruby tienden a favorecer al operador de la pala (<<) sobre el operador más igual a (+ =) al construir cadenas. ¿Por qué?

Supongo que implica velocidad, pero no entiendo la acción debajo del capó que haría que el operador de la pala sea más rápido.

¿Alguien podría explicar los detalles detrás de esta preferencia?

erinbrown
fuente
44
El operador de pala modifica el objeto String en lugar de crear un nuevo objeto String (memoria de costos). ¿No es bonita la sintaxis? cf. Java y .NET tienen clases de StringBuilder
Coronel Panic

Respuestas:

257

Prueba:

a = 'foo'
a.object_id #=> 2154889340
a << 'bar'
a.object_id #=> 2154889340
a += 'quux'
a.object_id #=> 2154742560

Así que <<altera la cadena original en lugar de crear uno nuevo. La razón de esto es que en ruby a += bes una abreviatura sintáctica para a = a + b(lo mismo ocurre con los otros <op>=operadores) que es una asignación. Por otro lado, <<hay un alias concat()que altera el receptor en el lugar.

noodl
fuente
3
Gracias noodl! Entonces, en esencia, ¿<< es más rápido porque no crea nuevos objetos?
erinbrown
1
Este punto de referencia dice que Array#joines más lento que el uso <<.
Andrew Grimm
55
Uno de los chicos de EdgeCase ha publicado una explicación con números de rendimiento: Un poco más sobre cuerdas
Cincinnati Joe
8
El enlace @CincinnatiJoe anterior parece estar roto, aquí hay uno nuevo: Un poco más sobre cadenas
jasoares
Para los usuarios de Java: el operador '+' en Ruby corresponde a la adición a través del objeto StringBuilder y '<<' corresponde a la concatenación de objetos String
nanosoft
79

Prueba de rendimiento:

#!/usr/bin/env ruby

require 'benchmark'

Benchmark.bmbm do |x|
  x.report('+= :') do
    s = ""
    10000.times { s += "something " }
  end
  x.report('<< :') do
    s = ""
    10000.times { s << "something " }
  end
end

# Rehearsal ----------------------------------------
# += :   0.450000   0.010000   0.460000 (  0.465936)
# << :   0.010000   0.000000   0.010000 (  0.009451)
# ------------------------------- total: 0.470000sec
# 
#            user     system      total        real
# += :   0.270000   0.010000   0.280000 (  0.277945)
# << :   0.000000   0.000000   0.000000 (  0.003043)
Nemo157
fuente
70

Un amigo que está aprendiendo Ruby como su primer lenguaje de programación me hizo esta misma pregunta mientras revisaba Strings in Ruby en la serie Ruby Koans. Se lo expliqué usando la siguiente analogía;

Tiene un vaso de agua que está medio lleno y necesita rellenarlo.

La primera forma de hacerlo es tomando un vaso nuevo, llenándolo hasta la mitad con agua de un grifo y luego usando este segundo vaso medio lleno para rellenar el vaso. Hace esto cada vez que necesita rellenar su vaso.

La segunda forma de tomar su vaso medio lleno y simplemente llenarlo con agua directamente del grifo.

Al final del día, tendría que limpiar más vasos si elige elegir un vaso nuevo cada vez que necesite rellenarlo.

Lo mismo se aplica al operador de pala y al operador más igual. Además, el operador igual elige un nuevo 'vaso' cada vez que necesita rellenar su vaso, mientras que el operador de la pala solo toma el mismo vaso y lo rellena. Al final del día, más colección de 'vidrio' para el operador igual de Plus.

Kibet Yegon
fuente
2
Gran analogía, me encantó.
GMA
55
Gran analogía pero terribles conclusiones. Tendrías que agregar que alguien más limpia los anteojos para que no tengas que preocuparte por ellos.
Filip Bartuzi
1
Gran analogía, creo que llega a una buena conclusión. Creo que se trata menos de quién tiene que limpiar el vidrio y más sobre la cantidad de vasos utilizados. Se podría imaginar que ciertas aplicaciones están superando los límites de la memoria en sus máquinas y que esas máquinas solo pueden limpiar una cierta cantidad de anteojos a la vez.
Charlie L
11

Esta es una vieja pregunta, pero acabo de encontrarla y no estoy completamente satisfecho con las respuestas existentes. Hay muchos puntos buenos acerca de que la pala << es más rápida que la concatenación + =, pero también hay una consideración semántica.

La respuesta aceptada de @noodl muestra que << modifica el objeto existente en su lugar, mientras que + = crea un nuevo objeto. Por lo tanto, debe tener en cuenta si desea que todas las referencias a la cadena reflejen el nuevo valor, o si desea dejar solo las referencias existentes y crear un nuevo valor de cadena para usar localmente. Si necesita todas las referencias para reflejar el valor actualizado, debe usar <<. Si desea dejar solo otras referencias, entonces necesita usar + =.

Un caso muy común es que solo hay una única referencia a la cadena. En este caso, la diferencia semántica no importa y es natural preferir << debido a su velocidad.

Tony
fuente
10

Debido a que es más rápido / no crea una copia de la cadena <-> el recolector de basura no necesita ejecutarse.

más asqueroso
fuente
Si bien las respuestas anteriores brindan más detalles, este es el único que las reúne para obtener la respuesta completa. La clave aquí parece estar en el sentido de la redacción de su "construcción de cadenas", lo que implica que no quiere o necesita las cadenas originales.
Drew Verlee
Esta respuesta se basa en una premisa falsa: tanto la asignación como la liberación de objetos de corta duración son esencialmente gratuitas en cualquier GC moderno medio decente. Es al menos tan rápido como la asignación de pila en C y significativamente más rápido que malloc/ free. Además, algunas implementaciones más modernas de Ruby probablemente optimizarán la asignación de objetos y la concatenación de cadenas por completo. OTOH, la mutación de objetos es terrible para el rendimiento de GC.
Jörg W Mittag
4

Si bien la mayoría de las respuestas +=son más lentas porque crea una nueva copia, ¡es importante tener en cuenta eso +=y << no son intercambiables! Desea usar cada uno en diferentes casos.

El uso <<también alterará las variables a las que se apunta b. Aquí también mutamos acuando no queremos hacerlo.

2.3.1 :001 > a = "hello"
 => "hello"
2.3.1 :002 > b = a
 => "hello"
2.3.1 :003 > b << " world"
 => "hello world"
2.3.1 :004 > a
 => "hello world"

Debido a que +=hace una nueva copia, también deja las variables que la señalan sin cambios.

2.3.1 :001 > a = "hello"
 => "hello"
2.3.1 :002 > b = a
 => "hello"
2.3.1 :003 > b += " world"
 => "hello world"
2.3.1 :004 > a
 => "hello"

¡Comprender esta distinción puede ahorrarle muchos dolores de cabeza cuando se trata de bucles!

Joseph Cho
fuente
2

Si bien no es una respuesta directa a su pregunta, ¿por qué The Fully Upturned Bin siempre ha sido uno de mis artículos favoritos de Ruby? También contiene información sobre cadenas con respecto a la recolección de basura.

Michael Kohl
fuente
Gracias por el consejo, Michael! Todavía no he llegado tan lejos en Ruby, pero definitivamente será útil en el futuro.
erinbrown