Warum wird der Schaufeloperator (<<) beim Erstellen eines Strings in Ruby dem Plus-Gleichen (+ =) vorgezogen?

156

Ich arbeite durch Ruby Koans.

Das test_the_shovel_operator_modifies_the_original_stringKoan in about_strings.rb enthält den folgenden Kommentar:

Ruby-Programmierer bevorzugen beim Aufbau von Strings den Schaufeloperator (<<) gegenüber dem Plus-Gleichheitsoperator (+ =). Warum?

Ich vermute, es geht um Geschwindigkeit, aber ich verstehe die Aktion unter der Motorhaube nicht, die dazu führen würde, dass der Schaufelbediener schneller wird.

Könnte jemand bitte die Details hinter dieser Präferenz erklären?

erinbrown
quelle
4
Der Schaufeloperator ändert das String-Objekt, anstatt ein neues String-Objekt zu erstellen (Kostenspeicher). Ist die Syntax nicht hübsch? vgl. Java und .NET haben StringBuilder-Klassen
Colonel Panic

Antworten:

257

Beweis:

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

So <<ändert sich die ursprüngliche Zeichenfolge , anstatt eine neue zu erstellen. Der Grund dafür ist, dass in Ruby a += beine syntaktische Abkürzung für a = a + b(dasselbe gilt für die anderen <op>=Operatoren) eine Zuweisung ist. Auf der anderen Seite <<befindet sich ein Alias, der concat()den Empfänger an Ort und Stelle verändert.

Nudel
quelle
3
Danke, Nudel! Das << ist also im Wesentlichen schneller, weil es keine neuen Objekte erstellt?
erinbrown
1
Dieser Benchmark besagt, dass dies Array#joinlangsamer ist als die Verwendung <<.
Andrew Grimm
5
Einer der EdgeCase-Leute hat eine Erklärung mit Leistungszahlen veröffentlicht: Ein bisschen mehr über Streicher
Cincinnati Joe
8
Der obige @ CincinnatiJoe-Link scheint defekt zu sein, hier ist ein neuer: Ein bisschen mehr über Strings
jasoares
Für Java-Leute: Der Operator '+' in Ruby entspricht dem Anhängen über das StringBuilder-Objekt und '<<' entspricht der Verkettung von String-Objekten
nanosoft
79

Leistungsnachweis:

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

Ein Freund, der Ruby als seine erste Programmiersprache lernt, stellte mir dieselbe Frage, als er Strings in Ruby in der Ruby Koans-Reihe durchging. Ich erklärte es ihm anhand der folgenden Analogie:

Sie haben ein Glas Wasser, das halb voll ist, und Sie müssen Ihr Glas nachfüllen.

Zuerst nehmen Sie ein neues Glas, füllen es zur Hälfte mit Wasser aus einem Wasserhahn und füllen dieses Trinkglas mit diesem zweiten halb vollen Glas nach. Sie tun dies jedes Mal, wenn Sie Ihr Glas nachfüllen müssen.

Die zweite Möglichkeit ist, dass Sie Ihr halb volles Glas nehmen und es direkt aus dem Wasserhahn mit Wasser auffüllen.

Am Ende des Tages müssten Sie mehr Gläser reinigen, wenn Sie jedes Mal ein neues Glas auswählen, wenn Sie Ihr Glas nachfüllen müssen.

Gleiches gilt für den Schaufelbediener und den Plus-Gleichheitsbetreiber. Außerdem wählt der gleiche Bediener jedes Mal ein neues „Glas“ aus, wenn er sein Glas nachfüllen muss, während der Schaufelbediener nur das gleiche Glas nimmt und es nachfüllt. Am Ende des Tages mehr 'Glas'-Sammlung für den Plus-Gleichberechtigten.

Kibet Yegon
quelle
2
Tolle Analogie, liebte es.
GMA
5
große Analogie, aber schreckliche Schlussfolgerungen. Sie müssten hinzufügen, dass Gläser von jemand anderem gereinigt werden, damit Sie sich nicht um sie kümmern müssen.
Filip Bartuzi
1
Tolle Analogie, ich denke, es kommt zu einem guten Ergebnis. Ich denke, es geht weniger darum, wer das Glas reinigen muss, als vielmehr um die Anzahl der überhaupt verwendeten Gläser. Sie können sich vorstellen, dass bestimmte Anwendungen die Speichergrenzen ihrer Computer überschreiten und dass diese Computer jeweils nur eine bestimmte Anzahl von Gläsern reinigen können.
Charlie L
11

Dies ist eine alte Frage, aber ich bin nur darauf gestoßen und mit den vorhandenen Antworten nicht ganz zufrieden. Es gibt viele gute Punkte, wenn die Schaufel << schneller als die Verkettung + = ist, aber es gibt auch eine semantische Überlegung.

Die akzeptierte Antwort von @noodl zeigt, dass << das vorhandene Objekt an Ort und Stelle ändert, während + = ein neues Objekt erstellt. Sie müssen also überlegen, ob alle Verweise auf die Zeichenfolge den neuen Wert widerspiegeln sollen, oder ob Sie die vorhandenen Verweise in Ruhe lassen und einen neuen Zeichenfolgenwert erstellen möchten, der lokal verwendet werden soll. Wenn Sie alle Referenzen benötigen, um den aktualisierten Wert wiederzugeben, müssen Sie << verwenden. Wenn Sie andere Referenzen in Ruhe lassen möchten, müssen Sie + = verwenden.

Ein sehr häufiger Fall ist, dass es nur einen einzigen Verweis auf die Zeichenfolge gibt. In diesem Fall spielt der semantische Unterschied keine Rolle und es ist natürlich, << wegen seiner Geschwindigkeit zu bevorzugen.

Tony
quelle
10

Da es schneller ist / keine Kopie der Zeichenfolge erstellt, muss <-> Garbage Collector nicht ausgeführt werden.

gröber
quelle
Während die obigen Antworten detaillierter sind, ist dies die einzige, die sie für die vollständige Antwort zusammenfasst. Der Schlüssel hier scheint im Wortlaut Ihres "Aufbaus von Strings" zu liegen, was bedeutet, dass Sie die Original-Strings nicht wollen oder brauchen.
Drew Verlee
Diese Antwort basiert auf einer falschen Prämisse: Sowohl das Zuweisen als auch das Freigeben kurzlebiger Objekte ist in jedem halbwegs anständigen modernen GC im Wesentlichen kostenlos. Es ist mindestens so schnell wie die Stapelzuweisung in C und deutlich schneller als malloc/ free. Einige modernere Ruby-Implementierungen werden wahrscheinlich die Objektzuordnung und die Verkettung von Zeichenfolgen vollständig optimieren. OTOH, mutierende Objekte sind für die GC-Leistung schrecklich.
Jörg W Mittag
4

Während die meisten Antworten +=langsamer sind, weil eine neue Kopie erstellt wird, ist es wichtig, dies zu berücksichtigen +=und << nicht austauschbar zu sein! Sie möchten jeweils in verschiedenen Fällen verwenden.

Durch <<die Verwendung werden auch alle Variablen geändert, auf die verwiesen wird b. Hier mutieren wir auch, awenn wir nicht wollen.

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"

Da +=eine neue Kopie erstellt wird, bleiben auch alle Variablen, die darauf verweisen, unverändert.

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"

Das Verstehen dieser Unterscheidung kann Ihnen viele Kopfschmerzen ersparen, wenn Sie mit Schleifen arbeiten!

Joseph Cho
quelle
2

Obwohl dies keine direkte Antwort auf Ihre Frage ist, war The Fully Upturned Bin immer einer meiner Lieblingsartikel von Ruby. Es enthält auch einige Informationen zu Zeichenfolgen in Bezug auf die Speicherbereinigung.

Michael Kohl
quelle
Danke für den Tipp, Michael! Ich bin in Ruby noch nicht so weit gekommen, aber es wird sich in Zukunft definitiv als nützlich erweisen.
erinbrown