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?


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.


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)

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.


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.


10

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


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!


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.


Obrigado pela dica, Michael! Ainda não cheguei tão longe em Ruby, mas com certeza será útil no futuro.
Erin Brown
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.