Pourquoi l'opérateur de pelle (<<) est-il préféré à plus-égal (+ =) lors de la construction d'une chaîne en Ruby?

156

Je travaille avec Ruby Koans.

Le test_the_shovel_operator_modifies_the_original_stringKoan dans about_strings.rb comprend le commentaire suivant:

Les programmeurs Ruby ont tendance à préférer l'opérateur de pelle (<<) à l'opérateur plus égal (+ =) lors de la création de chaînes. Pourquoi?

Je suppose que cela implique de la vitesse, mais je ne comprends pas l'action sous le capot qui ferait que l'opérateur de la pelle serait plus rapide.

Quelqu'un pourrait-il expliquer les détails de cette préférence?

Erinbrown
la source
4
L'opérateur de pelle modifie l'objet String au lieu de créer un nouvel objet String (coût de la mémoire). La syntaxe n'est-elle pas jolie? cf. Java et .NET ont des classes StringBuilder
Colonel Panic

Réponses:

257

Preuve:

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

<<Modifie donc la chaîne d'origine plutôt que d'en créer une nouvelle. La raison en est que dans ruby a += best un raccourci syntaxique pour a = a + b(il en va de même pour les autres <op>=opérateurs) qui est une affectation. D'autre part, <<un alias concat()modifie le récepteur en place.

noodl
la source
3
Merci, noodl! Donc, en substance, le << est plus rapide car il ne crée pas de nouveaux objets?
erinbrown
1
Ce benchmark dit que Array#joinc'est plus lent que d'utiliser <<.
Andrew Grimm
5
L'un des gars d'EdgeCase a publié une explication avec des chiffres de performance: Un peu plus sur les cordes
Cincinnati Joe
8
Le lien @CincinnatiJoe ci-dessus semble être rompu, en voici un nouveau: Un peu plus sur les chaînes
jasoares
Pour les utilisateurs de Java: l'opérateur '+' dans Ruby correspond à l'ajout via l'objet StringBuilder et '<<' correspond à la concaténation d'objets String
nanosoft
79

Preuve de performance:

#!/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
la source
70

Un ami qui apprend Ruby comme son premier langage de programmation m'a posé la même question en parcourant Strings in Ruby sur la série Ruby Koans. Je lui ai expliqué en utilisant l'analogie suivante;

Vous avez un verre d'eau à moitié plein et vous devez remplir votre verre.

Tout d'abord, prenez un nouveau verre, remplissez-le à moitié avec de l'eau du robinet, puis utilisez ce deuxième verre à moitié plein pour remplir votre verre à boire. Vous faites cela chaque fois que vous devez remplir votre verre.

La deuxième façon de prendre votre verre à moitié plein et de le remplir avec de l'eau directement du robinet.

À la fin de la journée, vous auriez plus de verres à nettoyer si vous choisissez de choisir un nouveau verre chaque fois que vous avez besoin de remplir votre verre.

Il en va de même pour l'opérateur de la pelle et l'opérateur plus égal. De plus, l'opérateur égal choisit un nouveau `` verre '' chaque fois qu'il a besoin de remplir son verre tandis que l'opérateur de la pelle prend juste le même verre et le remplit. À la fin de la journée, plus de collection de «verre» pour l'opérateur Plus égal.

Kibet Yegon
la source
2
Grande analogie, j'ai adoré.
GMA du
5
grande analogie mais terribles conclusions. Il faudrait ajouter que les verres sont nettoyés par quelqu'un d'autre pour que vous n'ayez pas à vous en soucier.
Filip Bartuzi
1
Grande analogie, je pense que cela arrive à une belle conclusion. Je pense qu'il s'agit moins de savoir qui doit nettoyer le verre que du nombre de verres utilisés. On peut imaginer que certaines applications repoussent les limites de la mémoire sur leurs machines et que ces machines ne peuvent nettoyer qu'un certain nombre de verres à la fois.
Charlie L
11

C'est une vieille question, mais je viens de la parcourir et je ne suis pas entièrement satisfait des réponses existantes. Il y a beaucoup de bons points à propos de la pelle << étant plus rapide que la concaténation + =, mais il y a aussi une considération sémantique.

La réponse acceptée de @noodl montre que << modifie l'objet existant en place, alors que + = crée un nouvel objet. Vous devez donc déterminer si vous souhaitez que toutes les références à la chaîne reflètent la nouvelle valeur ou si vous souhaitez laisser les références existantes seules et créer une nouvelle valeur de chaîne à utiliser localement. Si vous avez besoin que toutes les références reflètent la valeur mise à jour, vous devez utiliser <<. Si vous souhaitez laisser les autres références seules, vous devez utiliser + =.

Un cas très courant est qu'il n'y a qu'une seule référence à la chaîne. Dans ce cas, la différence sémantique n'a pas d'importance et il est naturel de préférer << à cause de sa rapidité.

Tony
la source
10

Parce que c'est plus rapide / ne crée pas de copie de la chaîne <-> garbage collector n'a pas besoin de s'exécuter.

plus grossier
la source
Bien que les réponses ci-dessus donnent plus de détails, c'est la seule qui les rassemble pour la réponse complète. La clé ici semble être dans le sens du libellé de votre "construction de chaînes", cela implique que vous ne voulez pas ou n'avez pas besoin des chaînes d'origine.
Drew Verlee
Cette réponse est basée sur une fausse prémisse: l'allocation et la libération d'objets de courte durée sont essentiellement gratuites dans tout GC moderne à mi-chemin décent. C'est au moins aussi rapide que l'allocation de pile en C et nettement plus rapide que malloc/ free. En outre, certaines implémentations Ruby plus modernes optimiseront probablement l'allocation d'objets et la concaténation de chaînes complètement. OTOH, la mutation des objets est terrible pour les performances du GC.
Jörg W Mittag
4

Bien que la couverture de la majorité des réponses +=soit plus lente car elle crée une nouvelle copie, il est important de garder à l'esprit cela +=et << ne le sont pas interchangeables! Vous souhaitez utiliser chacun dans des cas différents.

L'utilisation <<modifiera également toutes les variables pointées b. Ici, nous muterons également alorsque nous ne le voulons pas.

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"

Parce que +=fait une nouvelle copie, il laisse également toutes les variables qui pointent vers lui inchangées.

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"

Comprendre cette distinction peut vous éviter beaucoup de maux de tête lorsque vous avez affaire à des boucles!

Joseph Cho
la source
2

Bien que ce ne soit pas une réponse directe à votre question, pourquoi The Fully Upturned Bin a toujours été l'un de mes articles Ruby préférés. Il contient également des informations sur les chaînes concernant le garbage collection.

Michael Kohl
la source
Merci pour la pointe, Michael! Je ne suis pas encore allé aussi loin dans Ruby, mais cela me sera certainement utile à l'avenir.
erinbrown