Perché l'operatore di pala (<<) è preferito rispetto a più-uguale (+ =) quando si costruisce una stringa in Ruby?


156

Sto lavorando attraverso Ruby Koans.

Il test_the_shovel_operator_modifies_the_original_stringKoan in about_strings.rb include il seguente commento:

I programmatori di Ruby tendono a favorire l'operatore pala (<<) rispetto all'operatore più uguale (+ =) durante la creazione di stringhe. Perché?

La mia ipotesi è che riguardi la velocità, ma non capisco l'azione sotto il cofano che renderebbe l'operatore della pala più veloce.

Qualcuno sarebbe in grado di spiegare i dettagli dietro questa preferenza?


4
L'operatore pala modifica l'oggetto String anziché creare un nuovo oggetto String (costing memory). La sintassi non è carina? cf. Java e .NET hanno classi StringBuilder
Colonnello Panic,

Risposte:


257

Prova:

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

Quindi <<modifica la stringa originale anziché crearne una nuova. La ragione di ciò è che in ruby a += bc'è una scorciatoia sintattica per a = a + b(lo stesso vale per gli altri <op>=operatori) che è un compito. D'altra parte <<è un alias concat()che altera il ricevitore sul posto.


3
Grazie, noodl! Quindi, in sostanza, il << è più veloce perché non crea nuovi oggetti?
Erinbrown,

1
Questo benchmark afferma che Array#joinè più lento dell'uso <<.
Andrew Grimm,

5
Uno dei ragazzi di EdgeCase ha pubblicato una spiegazione con i numeri delle prestazioni: A Little More About Strings
Cincinnati Joe

8
Il link sopra @CincinnatiJoe sembra essere rotto, eccone uno nuovo: Un po 'di più sulle stringhe
jasoares

Per la gente di Java: l'operatore '+' in Ruby corrisponde all'aggiunta tramite l'oggetto StringBuilder e '<<' corrisponde alla concatenazione degli oggetti String
nanosoft,

79

Prova di prestazione:

#!/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)

70

Un amico che sta imparando Ruby come suo primo linguaggio di programmazione mi ha posto questa stessa domanda mentre attraversava Strings in Ruby nella serie Ruby Koans. Gliel'ho spiegato usando la seguente analogia;

Hai un bicchiere d'acqua mezzo pieno e devi riempire il bicchiere.

Per prima cosa lo fai prendendo un nuovo bicchiere, riempendolo a metà con acqua da un rubinetto e quindi usando questo secondo bicchiere mezzo pieno per riempire il bicchiere. Lo fai ogni volta che devi riempire il bicchiere.

Il secondo modo in cui prendi il bicchiere mezzo pieno e lo riempi di acqua direttamente dal rubinetto.

Alla fine della giornata, avresti più bicchieri da pulire se scegli di scegliere un nuovo bicchiere ogni volta che è necessario riempire il bicchiere.

Lo stesso vale per l'operatore della pala e l'operatore più uguale. Inoltre, l'operatore uguale preleva un nuovo "bicchiere" ogni volta che deve riempire il bicchiere mentre l'operatore della pala prende solo lo stesso bicchiere e lo riempie. Alla fine della giornata più raccolta 'vetro' per l'operatore Plus uguale.


2
Grande analogia, adorabile.
GMA,

5
grande analogia ma conclusioni terribili. Dovresti aggiungere che gli occhiali vengono puliti da qualcun altro, quindi non devi preoccupartene.
Filip Bartuzi,

1
Grande analogia, penso che giunga a una buona conclusione. Penso che dipenda meno da chi deve pulire il vetro e più dal numero di vetri utilizzati. Puoi immaginare che alcune applicazioni stiano spingendo i limiti della memoria sui loro computer e che quelle macchine possano pulire solo un certo numero di occhiali alla volta.
Charlie L

11

Questa è una vecchia domanda, ma l'ho appena incontrata e non sono completamente soddisfatto delle risposte esistenti. Ci sono molti punti positivi sul fatto che la pala << sia più veloce della concatenazione + =, ma c'è anche una considerazione semantica.

La risposta accettata da @noodl mostra che << modifica l'oggetto esistente, mentre + = crea un nuovo oggetto. Quindi è necessario considerare se si desidera che tutti i riferimenti alla stringa riflettano il nuovo valore o si desidera lasciare soli i riferimenti esistenti e creare un nuovo valore di stringa da utilizzare localmente. Se hai bisogno di tutti i riferimenti per riflettere il valore aggiornato, devi usare <<. Se vuoi lasciare altri riferimenti da solo, devi usare + =.

Un caso molto comune è che esiste un solo riferimento alla stringa. In questo caso, la differenza semantica non ha importanza ed è naturale preferire << per la sua velocità.


10

Perché è più veloce / non crea una copia della stringa <-> garbage collector non ha bisogno di essere eseguito.


Mentre le risposte di cui sopra forniscono maggiori dettagli, questa è l'unica che le mette insieme per la risposta completa. La chiave qui sembra essere nel senso del tuo "costruire stringhe" implica che non vuoi o non hai bisogno delle stringhe originali.
Ha disegnato Verlee

Questa risposta si basa su una premessa errata: allocare e liberare oggetti di breve durata è essenzialmente gratuito in qualsiasi GC moderno a metà decente. È almeno veloce quanto l'allocazione dello stack in C e significativamente più veloce di malloc/ free. Inoltre, alcune implementazioni Ruby più moderne probabilmente ottimizzeranno completamente l'allocazione degli oggetti e la concatenazione delle stringhe. OTOH, gli oggetti mutanti sono terribili per le prestazioni del GC.
Jörg W Mittag,

4

Mentre la maggior parte delle risposte +=è più lenta perché crea una nuova copia, è importante tenerlo presente +=e << non lo è intercambiabili! Si desidera utilizzare ciascuno in diversi casi.

L'uso <<modifica anche le variabili indicate b. Qui mutiamo anche aquando potremmo non volerlo.

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"

Perché +=crea una nuova copia, lascia invariate anche le variabili che la puntano.

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"

Comprendere questa distinzione può farti risparmiare un sacco di mal di testa quando hai a che fare con i loop!


2

Pur non essendo una risposta diretta alla tua domanda, perché The Fully Upturned Bin è sempre stato uno dei miei articoli preferiti di Ruby. Contiene inoltre alcune informazioni sulle stringhe relative alla garbage collection.


Grazie per la punta, Michael! Non sono ancora arrivato così lontano in Ruby, ma sicuramente tornerà utile in futuro.
Erinbrown,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.