Почему оператор лопатой (<<) предпочтительнее, чем плюс-равно (+ =) при построении строки в Ruby?

156

Я работаю через Руби Коанс.

test_the_shovel_operator_modifies_the_original_stringKoan в about_strings.rb включает следующий комментарий:

При построении строк программисты на Ruby предпочитают оператор лопатки (<<) перед оператором плюс-равно (+ =). Зачем?

Я предполагаю, что это связано со скоростью, но я не понимаю действия под капотом, которые заставили бы оператора лопаты работать быстрее.

Кто-нибудь сможет объяснить подробности этого предпочтения?

Эрин Браун
источник
4
Оператор shovel изменяет объект String, а не создает новый объект String (память затрат). Разве синтаксис не симпатичен? ср У Java и .NET есть классы StringBuilder
полковник Паник

Ответы:

257

Доказательство:

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

Так что <<изменяет исходную строку, а не создает новую. Причина этого заключается в том, что в ruby a += bиспользуется синтаксическое сокращение a = a + b(то же самое относится и к другим <op>=операторам), которое является присваиванием. С другой стороны, <<псевдоним, concat()который изменяет приемник на месте.

noodl
источник
3
Спасибо, лапша! Итак, по сути, << быстрее, потому что он не создает новые объекты?
erinbrown
1
Этот тест говорит, что Array#joinэто медленнее, чем использование <<.
Эндрю Гримм
5
Один из ребят из EdgeCase опубликовал объяснение с номерами производительности: немного больше о струнах
Цинциннати Джо,
8
Приведенная выше ссылка @CincinnatiJoe, похоже, не работает, вот новая: немного больше о строках
jasoares
Для Java-людей: оператор «+» в Ruby соответствует добавлению через объект StringBuilder, а «<<» соответствует объединению объектов String
nanosoft
79

Доказательство производительности:

#!/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
источник
70

Друг, который изучает Ruby как его первый язык программирования, задал мне тот же вопрос, когда просматривал Strings in Ruby в серии Ruby Koans. Я объяснил это ему, используя следующую аналогию;

У вас есть стакан воды, который наполовину полон, и вам нужно пополнить свой стакан.

Первый способ сделать это - взять новый стакан, наполовину наполнив его водой из-под крана, а затем использовать этот второй наполовину полный стакан, чтобы наполнить свой стакан. Вы делаете это каждый раз, когда вам нужно пополнить свой стакан.

Второй способ - взять наполовину полный стакан и просто наполнить его водой прямо из-под крана.

В конце дня у вас будет больше очков для чистки, если вы решите выбрать новый стакан каждый раз, когда вам нужно пополнить свой стакан.

То же самое относится к оператору лопаты и оператору равенства плюс. Плюс оператор «равный» выбирает новый «стакан» каждый раз, когда ему необходимо пополнить свой стакан, в то время как оператор лопатки просто берет тот же стакан и наполняет его. В конце дня больше «стеклянной» коллекции для оператора Plus Plus.

Кибет Егон
источник
2
Отличная аналогия, понравилось.
GMA
5
отличная аналогия, но ужасные выводы. Вы должны добавить, что очки чистятся кем-то другим, поэтому вам не нужно заботиться о них.
Филипп Бартузи
1
Отличная аналогия, я думаю, что она приходит к прекрасному выводу. Я думаю, что дело не в том, кто должен чистить стекло, а в том, сколько очков вообще использовалось. Вы можете себе представить, что определенные приложения расширяют границы памяти на своих машинах и что эти машины могут чистить только определенное количество очков за раз.
Чарли Л
11

Это старый вопрос, но я просто наткнулся на него, и я не полностью удовлетворен существующими ответами. Есть много хороших моментов в том, что лопата << быстрее, чем конкатенация + =, но есть и семантическое соображение.

Принятый ответ от @noodl показывает, что << изменяет существующий объект на месте, тогда как + = создает новый объект. Поэтому вам нужно подумать, хотите ли вы, чтобы все ссылки на строку отражали новое значение, или вы хотите оставить существующие ссылки в покое и создать новое строковое значение для локального использования. Если вам нужны все ссылки, чтобы отразить обновленное значение, тогда вам нужно использовать <<. Если вы хотите оставить другие ссылки в покое, тогда вам нужно использовать + =.

Очень распространенным случаем является только одна ссылка на строку. В этом случае семантическая разница не имеет значения, и естественно предпочесть << из-за ее скорости.

Тони
источник
10

Поскольку он быстрее / не создает копию строки, <-> сборщик мусора запускать не нужно.

грубее
источник
В то время как вышеупомянутые ответы дают больше деталей, это единственный, который соединяет их для полного ответа. Кажется, что ключ здесь в том смысле, в каком вы «строите строки», это означает, что вы не хотите или не нуждаетесь в исходных строках.
Дрю Верли
Этот ответ основан на ложной предпосылке: как размещение, так и освобождение недолговечных объектов в любом приличном современном GC практически бесплатно. Это по крайней мере так же быстро, как распределение стека в C и значительно быстрее, чем malloc/ free. Кроме того, некоторые более современные реализации Ruby, вероятно, полностью оптимизируют распределение объектов и конкатенацию строк. OTOH, мутирующие объекты ужасны для производительности GC.
Йорг Миттаг
4

Хотя большинство ответов покрывают +=медленнее, потому что создает новую копию, важно иметь в виду, что +=и << не взаимозаменяемы! Вы хотите использовать каждый в разных случаях.

Использование <<также изменит любые переменные, на которые указывают b. Здесь мы также мутируем, aкогда не хотим.

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"

Поскольку +=создает новую копию, она также оставляет все переменные, которые указывают на нее, без изменений.

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"

Понимание этого различия поможет вам избежать головной боли при работе с петлями!

Джозеф Чо
источник
2

Хотя это и не прямой ответ на ваш вопрос, почему « Полностью перевернутая корзина» всегда была одной из моих любимых статей на Ruby. Он также содержит некоторую информацию о строках в отношении сборки мусора.

Майкл Коля
источник
Спасибо за совет, Майкл! Я еще не дошел до такого уровня в Ruby, но он наверняка пригодится в будущем.
erinbrown