Dlaczego operator łopaty (<<) jest preferowany nad plus-równa się (+ =) podczas tworzenia łańcucha znaków w Rubim?


156

Pracuję przez Ruby Koans.

test_the_shovel_operator_modifies_the_original_stringKoan w about_strings.rb zawiera następującą uwagę:

Programiści Ruby mają tendencję do faworyzowania operatora łopaty (<<) zamiast operatora plus równa się (+ =) podczas tworzenia łańcuchów. Czemu?

Domyślam się, że wiąże się to z prędkością, ale nie rozumiem działania pod maską, które spowodowałoby, że operator łopaty byłby szybszy.

Czy ktoś mógłby wyjaśnić szczegóły tej preferencji?


4
Operator łopaty modyfikuje obiekt String, zamiast tworzyć nowy obiekt String (koszt pamięci). Czy składnia nie jest ładna? por. Java i .NET mają klasy StringBuilder
Colonel Panic

Odpowiedzi:


257

Dowód:

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

Więc <<zmienia oryginalny ciąg zamiast tworzenia nowego. Powodem tego jest to, że w ruby a += bjest skrótem składniowym a = a + b(to samo dotyczy innych <op>=operatorów), czyli przypisaniem. Z drugiej strony <<jest aliasem, concat()który zmienia odbiornik w miejscu.


3
Dzięki, makaron! Zatem w istocie << jest szybsze, ponieważ nie tworzy nowych obiektów?
erinbrown

1
Ten test porównawczy mówi, że Array#joinjest wolniejszy niż używanie <<.
Andrew Grimm,

5
Jeden z facetów z EdgeCase opublikował wyjaśnienie z liczbami występów: Trochę więcej o smyczkach
Cincinnati Joe

8
Powyższy link @CincinnatiJoe wydaje się być uszkodzony, tutaj jest nowy: Trochę więcej o
smyczkach

Dla osób pracujących w Javie: operator „+” w Rubim odpowiada dołączaniu przez obiekt StringBuilder, a „<<” odpowiada konkatenacji obiektów typu String
nanosoft

79

Dowód wydajności:

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

Znajomy, który uczy się Rubiego jako swojego pierwszego języka programowania, zadał mi to samo pytanie, przeglądając Strings in Ruby w serii Ruby Koans. Wyjaśniłem mu to za pomocą następującej analogii;

Masz szklankę wody, która jest do połowy pełna i musisz ją ponownie napełnić.

Najpierw weź nową szklankę, napełnij ją do połowy wodą z kranu, a następnie użyj drugiej do połowy pełnej szklanki do ponownego napełnienia szklanki. Robisz to za każdym razem, gdy potrzebujesz uzupełnić szklankę.

Drugi sposób, aby wziąć do połowy pełną szklankę i po prostu napełnić ją wodą prosto z kranu.

Pod koniec dnia będziesz mieć więcej szklanek do wyczyszczenia, jeśli zdecydujesz się wybrać nową szklankę za każdym razem, gdy będziesz musiał uzupełnić szklankę.

To samo dotyczy operatora łopaty i operatora plusa równego. Ponadto operator równorzędny wybiera nową „szklankę” za każdym razem, gdy trzeba ją uzupełnić, podczas gdy operator łopaty po prostu bierze tę samą szklankę i napełnia ją. Na koniec dnia kolejna zbiórka „szkła” dla operatora równorzędnego Plus.


2
Świetna analogia, bardzo mi się podobało.
GMA,

5
wielka analogia, ale straszne wnioski. Musiałbyś dodać, że okulary czyści ktoś inny, więc nie musisz się nimi przejmować.
Filip Bartuzi

1
Świetna analogia, myślę, że to dobry wniosek. Myślę, że mniej chodzi o to, kto musi czyścić szybę, a bardziej o liczbę używanych szklanek. Można sobie wyobrazić, że niektóre aplikacje przesuwają ograniczenia pamięci na swoich komputerach i że maszyny te mogą czyścić tylko określoną liczbę szklanek naraz.
Charlie L

11

To stare pytanie, ale właśnie je trafiłem i nie jestem w pełni zadowolony z istniejących odpowiedzi. Jest wiele dobrych punktów na temat tego, że łopata << jest szybsza niż konkatenacja + =, ale jest też kwestia semantyczna.

Zaakceptowana odpowiedź od @noodl pokazuje, że << modyfikuje istniejący obiekt w miejscu, podczas gdy + = tworzy nowy obiekt. Musisz więc zastanowić się, czy chcesz, aby wszystkie odwołania do ciągu odzwierciedlały nową wartość, czy też chcesz zostawić istniejące odniesienia w spokoju i utworzyć nową wartość ciągu do użytku lokalnego. Jeśli potrzebujesz, aby wszystkie odwołania odzwierciedlały zaktualizowaną wartość, musisz użyć znaku <<. Jeśli chcesz zostawić inne odniesienia w spokoju, musisz użyć + =.

Bardzo częstym przypadkiem jest to, że istnieje tylko jedno odniesienie do ciągu. W tym przypadku różnica semantyczna nie ma znaczenia i naturalne jest preferowanie znaku << ze względu na jego szybkość.


10

Ponieważ jest szybszy / nie tworzy kopii łańcucha <-> garbage collector nie musi działać.


Chociaż powyższe odpowiedzi dają więcej szczegółów, jest to jedyna, która łączy je razem, aby uzyskać pełną odpowiedź. Wydaje się, że kluczem tutaj jest sformułowanie „budowanie ciągów”, co oznacza, że ​​nie chcesz lub nie potrzebujesz oryginalnych ciągów.
Drew Verlee

Ta odpowiedź opiera się na fałszywej przesłance: zarówno przydzielanie, jak i zwalnianie krótkotrwałych obiektów jest zasadniczo bezpłatne w każdym w połowie przyzwoitym, nowoczesnym GC. Jest co najmniej tak szybka jak alokacja stosu w C i znacznie szybsza niż malloc/ free. Ponadto niektóre bardziej nowoczesne implementacje Rubiego prawdopodobnie całkowicie zoptymalizują alokację obiektów i łączenie ciągów znaków. OTOH, mutowanie obiektów jest straszne dla wydajności GC.
Jörg W Mittag,

4

Chociaż większość odpowiedzi +=jest wolniejsza, ponieważ tworzy nową kopię, ważne jest, aby o tym pamiętać +=i << nie można ich używać zamiennie! Chcesz użyć każdego w różnych przypadkach.

Użycie <<zmieni także wszelkie wskazywane zmienne b. Tutaj również mutujemy, akiedy możemy tego nie chcieć.

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"

Ponieważ +=tworzy nową kopię, pozostawia również niezmienione zmienne, które na nią wskazują.

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"

Zrozumienie tego rozróżnienia może zaoszczędzić wiele bólu głowy, gdy masz do czynienia z pętlami!


2

Chociaż nie jest to bezpośrednia odpowiedź na twoje pytanie, dlaczego The Fully Upturned Bin zawsze był jednym z moich ulubionych artykułów Ruby. Zawiera również informacje o ciągach znaków w odniesieniu do czyszczenia pamięci.


Dziękuję za wskazówkę, Michael! Nie zaszedłem jeszcze tak daleko w Rubim, ale na pewno przyda się w przyszłości.
erinbrown
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.