Por que o operador de escavadeira (<<) é preferido em vez de mais-igual (+ =) ao construir uma string em Ruby?

156

Estou trabalhando com Ruby Koans.

O test_the_shovel_operator_modifies_the_original_stringKoan em about_strings.rb inclui o seguinte comentário:

Os programadores Ruby tendem a favorecer o operador de escavadeira (<<) sobre o operador de mais iguais (+ =) ao criar cadeias. Por quê?

Meu palpite é que envolve velocidade, mas não entendo a ação sob o capô que faria com que o operador da escavadeira fosse mais rápido.

Alguém poderia explicar os detalhes por trás dessa preferência?

erinbrown
fonte
4
O operador shovel modifica o objeto String em vez de criar um novo objeto String (custando memória). A sintaxe não é bonita? cf. Java e .NET têm classes StringBuilder
Coronel Panic

Respostas:

257

Prova:

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

Portanto, <<altera a string original em vez de criar uma nova. A razão para isso é que, em ruby, a += bé uma abreviação sintática para a = a + b(o mesmo vale para os outros <op>=operadores), que é uma atribuição. Por outro lado, <<existe um apelido concat()que altera o receptor no local.

noodl
fonte
3
Obrigado, noodl! Então, em essência, o << é mais rápido porque não cria novos objetos?
Erin Brown
1
Este benchmark diz que Array#joiné mais lento que o uso <<.
Andrew Grimm
5
Um dos caras do EdgeCase postou uma explicação com os números de desempenho: Um pouco mais sobre as cordas
Cincinnati Joe
8
O link acima @CincinnatiJoe parece estar quebrado, aqui é um novo: Um Pouco Mais Sobre Cordas
jasoares
Para pessoas java: o operador '+' em Ruby corresponde ao acréscimo através do objeto StringBuilder e '<<' corresponde à concatenação de objetos String
nanosoft
79

Prova de desempenho:

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

Um amigo que está aprendendo Ruby como sua primeira linguagem de programação me fez a mesma pergunta enquanto passava por Strings in Ruby na série Ruby Koans. Expliquei a ele usando a seguinte analogia;

Você tem um copo de água pela metade e precisa recarregá-lo.

A primeira maneira de fazê-lo é pegar um copo novo, enchê-lo até a metade com água de uma torneira e usar esse segundo copo meio cheio para encher novamente o copo. Você faz isso toda vez que precisar encher seu copo.

A segunda maneira de pegar seu copo meio cheio e apenas enchê-lo com água diretamente da torneira.

No final do dia, você teria mais copos para limpar se escolher um copo novo toda vez que precisar recarregá-lo.

O mesmo se aplica ao operador de escavadeira e ao operador mais igual. Além disso, o operador igual escolhe um novo 'copo' toda vez que precisa reabastecer seu copo, enquanto o operador da escavadora apenas pega o mesmo copo e o recarrega. No final do dia, mais coleta de 'vidro' para o operador igual Plus.

Kibet Yegon
fonte
2
Ótima analogia, adorei.
GMA
5
grande analogia, mas conclusões terríveis. Você teria que acrescentar que os óculos são limpos por outra pessoa para que você não precise se preocupar com eles.
Filip Bartuzi
1
Ótima analogia, acho que chega a uma boa conclusão. Acho que é menos sobre quem precisa limpar o vidro e mais sobre o número de copos usados. Você pode imaginar que certos aplicativos estão aumentando os limites de memória em suas máquinas e que essas máquinas podem limpar apenas um certo número de óculos por vez.
Charlie L
11

Essa é uma pergunta antiga, mas eu a encontrei e não estou totalmente satisfeita com as respostas existentes. Há muitos pontos positivos sobre a pá ser mais rápida que a concatenação + =, mas também há uma consideração semântica.

A resposta aceita de @noodl mostra que << modifica o objeto existente, enquanto + = cria um novo objeto. Portanto, você deve considerar se deseja que todas as referências à sequência reflitam o novo valor ou se deseja deixar as referências existentes em paz e criar um novo valor para usar localmente. Se você precisar de todas as referências para refletir o valor atualizado, precisará usar <<. Se você quiser deixar outras referências em paz, precisará usar + =.

Um caso muito comum é que há apenas uma única referência à string. Nesse caso, a diferença semântica não importa e é natural preferir << devido à sua velocidade.

Tony
fonte
10

Porque é mais rápido / não cria uma cópia da cadeia de caracteres <-> o coletor de lixo não precisa ser executado.

mais grosseiro
fonte
Enquanto as respostas acima fornecem mais detalhes, este é o único que as reúne para obter a resposta completa. A chave aqui parece estar no sentido em que você "constrói as strings", implica que você não deseja ou precisa das strings originais.
Drew Verlee
Essa resposta é baseada em uma premissa falsa: alocar e liberar objetos de vida curta é essencialmente livre em qualquer GC moderno decente. É pelo menos tão rápido quanto a alocação de pilha em C e significativamente mais rápido que malloc/ free. Além disso, algumas implementações mais modernas do Ruby provavelmente otimizarão a alocação de objetos e a concatenação de cadeias completamente. OTOH, a mutação de objetos é terrível para o desempenho do GC.
Jörg W Mittag
4

Embora a maioria das respostas +=seja mais lenta porque cria uma nova cópia, é importante ter isso em mente +=e << não é intercambiável! Você deseja usar cada um em diferentes casos.

O uso <<também altera as variáveis ​​apontadas b. Aqui também mudamos aquando não queremos.

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"

Como +=faz uma nova cópia, também deixa inalteradas as variáveis ​​que estão apontando para ela.

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"

Entender essa distinção pode economizar muitas dores de cabeça quando você está lidando com loops!

Joseph Cho
fonte
2

Embora não seja uma resposta direta à sua pergunta, o motivo pelo qual The Fully Upturned Bin sempre foi um dos meus artigos favoritos sobre Ruby. Ele também contém algumas informações sobre cadeias de caracteres em relação à coleta de lixo.

Michael Kohl
fonte
Obrigado pela dica, Michael! Ainda não cheguei tão longe em Ruby, mas com certeza será útil no futuro.
Erin Brown