Unlike other languages, strings in ruby are mutable (meaning they can be altered without needing to create a new instance).

In languages where strings are immutable, the efficient way to build strings is to use some form of mutable objets like an Array to hold the parts of the string to built and concatenate them at the end.Thats why Java has a StringBuffer class to tackle exactly the such problem.

In ruby there are multiple ways to concatenate strings:

  • <<
  • +=
  • sprintf
  • string substitution
  first_string  = 'This is the first string.'
  second_string = 'This is the second string.'
  first_string  += second_string

   => "This is the first string.This is the second string."

  first_string  = 'This is the first string.'
  second_string = 'This is the second string.'
  first_string  << second_string

   => "This is the first string.This is the second string."

  first_string  = 'This is the first string.'
  second_string = 'This is the second string.'
  first_string  = sprintf '%s%s', first_string, second_string

   => "This is the first string.This is the second string."

  first_string  = 'This is the first string.'
  second_string = 'This is the second string.'
  first_string  = "#{first_string}#{second_string}"

   => "This is the first string.This is the second string."

+= operation builds a brand new string and assigns that new string to first_string. Lets take a closer look at the memory locations where the strings are stored at:

  > first_string  = 'This is the first string.'
  > first_string.object_id
    => 70236779674380 # Memory address the first_string is located at

  > second_string = 'This is the second string.'
  > second_string.object_id
    => 70236779585700 # Memory address the second_string is located at

  > first_string  += second_string
  > first_string.object_id
    => 70236779445580 # Memory address the new first_string is located at

As you can notice first_string now points to a totally different memory location, meaning that its a new string.

On the other side << keeps appending to the very same string without needing to create a new instance. Now lets take a look at its memory state:

  > first_string  = 'This is the first string.'
  > first_string.object_id
    => 70236779373080 # Memory address the first_string is located at

  > second_string = 'This is the second string.'
  > second_string.object_id
    => 70236779314860 # Memory address the second_string is located at

  > first_string  += second_string
  > first_string.object_id
    => 70236779373080 # Memory address still points to the original first_string's address

We can also see the results from the following benchmarks:

#!/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
  x.report('sprintf :') do
    s = ""
    10000.times { s = sprintf '%s%s', s, "something " }
  end
  x.report('substitution :') do
    s = ""
    10000.times { s = "#{s}something " }
  end
end

Rehearsal --------------------------------------------------
+= :             0.110000   0.100000   0.210000 (  0.206531)
<< :             0.000000   0.000000   0.000000 (  0.002501)
sprintf :        0.120000   0.090000   0.210000 (  0.212252)
substitution :   0.140000   0.190000   0.330000 (  0.328661)
----------------------------------------- total: 0.750000sec

                     user     system      total        real
+= :             0.100000   0.080000   0.180000 (  0.184592)
<< :             0.000000   0.000000   0.000000 (  0.001975)
sprintf :        0.120000   0.080000   0.200000 (  0.204484)
substitution :   0.140000   0.190000   0.330000 (  0.341059)

If you look at the last column you will notice the total amount of time it took each way of building strings.