Mengapa operator shovel (<<) lebih disukai daripada plus-sama (+ =) saat membuat string di Ruby?

156

Saya bekerja melalui Ruby Koans.

The test_the_shovel_operator_modifies_the_original_stringKoan di about_strings.rb termasuk komentar berikut:

Pemrogram Ruby cenderung lebih menyukai operator shovel (<<) daripada operator plus sama dengan (+ =) saat membangun string. Mengapa?

Dugaan saya adalah ini melibatkan kecepatan, tetapi saya tidak mengerti aksi di bawah kap yang akan menyebabkan operator shovel lebih cepat.

Apakah seseorang dapat menjelaskan detail di balik preferensi ini?

erinbrown
sumber
4
Operator shovel memodifikasi objek String daripada membuat objek String baru (memori biaya). Bukankah sintaksisnya cantik? lih. Java dan .NET memiliki kelas StringBuilder
Kolonel Panic

Jawaban:

257

Bukti:

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

Jadi <<ubah string asli daripada membuat yang baru. Alasan untuk ini adalah bahwa dalam ruby a += badalah singkatan sintaksis untuk a = a + b(yang sama berlaku untuk <op>=operator lain ) yang merupakan tugas. Di sisi lain <<adalah alias concat()yang mengubah penerima di tempat.

noodl
sumber
3
Terima kasih, noodl! Jadi, pada intinya, << lebih cepat karena tidak membuat objek baru?
erinbrown
1
Penghitungan ini mengatakan Array#joinlebih lambat daripada menggunakan <<.
Andrew Grimm
5
Salah satu orang EdgeCase telah memposting penjelasan dengan nomor kinerja: A Little More About Strings
Cincinnati Joe
8
Tautan @CincinnatiJoe di atas tampaknya rusak, di sini ada yang baru: A Little More About Strings
jasoares
Untuk orang-orang java: operator '+' di Ruby sesuai dengan penambahan melalui objek StringBuilder dan '<<' sesuai dengan penggabungan objek String
nanosoft
79

Bukti kinerja:

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

Seorang teman yang mempelajari Ruby sebagai bahasa pemrograman pertamanya bertanya kepada saya pertanyaan yang sama saat ini melalui Strings in Ruby pada seri Ruby Koans. Saya menjelaskannya kepadanya menggunakan analogi berikut;

Anda memiliki segelas air yang setengah penuh dan Anda perlu mengisi ulang gelas Anda.

Cara pertama Anda melakukannya dengan mengambil gelas baru, mengisinya setengah dengan air dari keran dan kemudian menggunakan gelas setengah penuh kedua ini untuk mengisi gelas minum Anda. Anda melakukan ini setiap kali Anda perlu mengisi ulang gelas Anda.

Cara kedua Anda mengambil setengah gelas penuh dan isi ulang dengan air langsung dari keran.

Pada akhir hari, Anda akan memiliki lebih banyak kacamata untuk dibersihkan jika Anda memilih untuk mengambil gelas baru setiap kali Anda perlu mengisi ulang gelas Anda.

Hal yang sama berlaku untuk operator shovel dan operator plus sama. Ditambah lagi operator yang sama mengambil 'gelas' baru setiap kali perlu mengisi ulang gelasnya sementara operator shovel hanya mengambil gelas yang sama dan mengisinya kembali. Pada akhirnya, lebih banyak koleksi 'gelas' untuk operator setara Plus.

Kibet Yegon
sumber
2
Analogi yang hebat, menyukainya.
GMA
5
analogi yang bagus tapi kesimpulan yang mengerikan. Anda harus menambahkan bahwa kacamata dibersihkan oleh orang lain sehingga Anda tidak perlu peduli dengan mereka.
Filip Bartuzi
1
Analogi yang bagus, saya pikir itu sampai pada kesimpulan yang bagus. Saya pikir ini kurang tentang siapa yang harus membersihkan gelas dan lebih banyak tentang jumlah kacamata yang digunakan sama sekali. Anda dapat membayangkan bahwa aplikasi tertentu mendorong batas memori pada mesin mereka dan bahwa mesin tersebut hanya dapat membersihkan sejumlah kacamata pada satu waktu.
Charlie L
11

Ini adalah pertanyaan lama, tetapi saya hanya berlari melewatinya dan saya tidak sepenuhnya puas dengan jawaban yang ada. Ada banyak poin bagus tentang shovel << lebih cepat daripada concatenation + =, tetapi ada juga pertimbangan semantik.

Jawaban yang diterima dari @noodl menunjukkan bahwa << memodifikasi objek yang ada di tempatnya, sedangkan + = membuat objek baru. Jadi, Anda perlu mempertimbangkan apakah Anda ingin semua referensi ke string mencerminkan nilai baru, atau apakah Anda ingin meninggalkan referensi yang ada sendirian dan membuat nilai string baru untuk digunakan secara lokal. Jika Anda membutuhkan semua referensi untuk mencerminkan nilai yang diperbarui, maka Anda perlu menggunakan <<. Jika Anda ingin meninggalkan referensi lain sendirian, maka Anda perlu menggunakan + =.

Kasus yang sangat umum adalah hanya ada satu referensi ke string. Dalam hal ini, perbedaan semantik tidak masalah dan itu wajar untuk memilih << karena kecepatannya.

Tony
sumber
10

Karena lebih cepat / tidak membuat salinan string <-> pengumpul sampah tidak perlu dijalankan.

lebih kotor
sumber
Sementara jawaban di atas memberikan lebih banyak detail, ini adalah satu-satunya yang menyatukannya untuk jawaban yang lengkap. Kuncinya di sini tampaknya dalam arti kata-kata Anda "membangun string" itu menyiratkan bahwa Anda tidak ingin atau membutuhkan string asli.
Drew Verlee
Jawaban ini didasarkan pada premis yang salah: baik mengalokasikan dan membebaskan objek berumur pendek pada dasarnya gratis di setiap GC modern yang setengah jalan. Paling tidak secepat alokasi stack di C dan secara signifikan lebih cepat dari malloc/ free. Juga, beberapa implementasi Ruby yang lebih modern mungkin akan mengoptimalkan alokasi objek dan penggabungan string sepenuhnya. OTOH, bermutasi objek sangat buruk untuk kinerja GC.
Jörg W Mittag
4

Meskipun sebagian besar jawaban mencakup +=lebih lambat karena membuat salinan baru, penting untuk diingat bahwa +=dan << tidak dapat dipertukarkan! Anda ingin menggunakan masing-masing dalam kasus yang berbeda.

Menggunakan <<juga akan mengubah variabel apa pun yang diarahkan b. Di sini kita juga bermutasi aketika kita mungkin tidak mau.

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"

Karena +=membuat salinan baru, itu juga meninggalkan variabel apa pun yang menunjuk ke sana tidak berubah.

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"

Memahami perbedaan ini dapat menghemat banyak sakit kepala saat Anda berurusan dengan loop!

Joseph Cho
sumber
2

Meskipun bukan jawaban langsung untuk pertanyaan Anda, mengapa The Fully Upturned Bin selalu menjadi salah satu artikel Ruby favorit saya. Ini juga berisi beberapa info tentang string terkait dengan pengumpulan sampah.

Michael Kohl
sumber
Terima kasih atas tipnya, Michael! Saya belum sampai sejauh itu di Ruby, tetapi pasti akan berguna di masa depan.
erinbrown