Last Updated: July 26, 2022
·
69.69K
· bitsbyte

+ or .concat, what is faster for appending string in Ruby?

Problem:

Yesterday I had to append around ~100-200K small strings (avg. length 7) to form a single large string . I had used + operator and it was very slow. To find a faster alternative I did following benchmarking experiment:

Solution:

TL;DR : use .concat instead of +

def bm_concat limit
  str = ""
  limit.times do
    str.concat("abcdefg")
  end
end

def bm_plus limit
  str = ""
  limit.times do
    str += "abcdefg"
  end
end

def diff t1, t2
  puts (t2-t1)
end

limit = ARGV[0]
which = ARGV[1]

puts "LIMIT: #{limit} , WHICH: #{which}"
if which == "plus"
  t = Time.now
  bm_plus limit.to_i
  t2 = Time.now
else
  t = Time.now
  bm_concat limit.to_i
  t2 = Time.now
end

diff(t,t2)

On replacing + with .concat in my originial code it was 4x faster!

Results of above code for various parameters are :

Picture

4 Responses
Add your response

'+' created new object every time and '.concat' works on the same object.
Thats why 'concat' is faster and that's why you should not use it every time.
Tip: '<<' is an alias for '.concat'.

over 1 year ago ·

@korin learned it the hard way ! :)

over 1 year ago ·

Expanded this test to also include:
str = "#{str}abcdefg"

Seems to be even slower. ~18 seconds for plus vs. ~23 seconds for the #{str} notation doing 100000 concats. Tried about 10 times each so not super reliable but seems pretty consistent.

Further testing with other concatenations seems to show that str << "abcdefg" is even faster than .concat. But not by much. I would like to hear if anyone else can reproduce these results.

over 1 year ago ·

Thanks - very interesting. I was also curious about the results for interpolation so I added to you code and used the Benchmark module to summarize the results.

Ruby Version 2.5.1
========================================

Running for 1000 iterations
----------------------------------------
       user     system      total        real
concat:  0.000202   0.000002   0.000204 (  0.000202)
    <<:  0.000340   0.000000   0.000340 (  0.000342)
  plus:  0.001203   0.000719   0.001922 (  0.001924)
interp:  0.001206   0.000805   0.002011 (  0.002013)

Running for 10000 iterations
----------------------------------------
       user     system      total        real
concat:  0.001593   0.000017   0.001610 (  0.001610)
    <<:  0.000987   0.000001   0.000988 (  0.000988)
  plus:  0.059315   0.048627   0.107942 (  0.110302)
interp:  0.057374   0.048208   0.105582 (  0.105909)

Running for 100000 iterations
----------------------------------------
       user     system      total        real
concat:  0.011506   0.002310   0.013816 (  0.013827)
    <<:  0.010209   0.000755   0.010964 (  0.011015)
  plus:  7.285013   7.813944  15.098957 ( 15.137750)
interp:  7.290111   7.737633  15.027744 ( 15.293222)

Running for 1000000 iterations
----------------------------------------
       user     system      total        real
concat:  0.114216   0.002944   0.117160 (  0.118727)
    <<:  0.103323   0.003833   0.107156 (  0.110718)
  plus: 426.376333 188.171333 614.547666 (619.664617)
interp:429.128582 195.009173 624.137755 (631.839058)

Not surprisingly the plus and interpolation results increase exponentially. concat and << appear to be linear.

And the code

def bm_concat limit
  str = ""
  limit.times do
    str.concat("abcdefg")
  end
end

def bm_append limit
  str = ""
  limit.times do
    str << "abcdefg"
  end
end

def bm_plus limit
  str = ""
  limit.times do
    str += "abcdefg"
  end
end

def bm_interp limit
  str = ""
  limit.times do
    str = "#{str}abcdefg"
  end
end

require 'benchmark'

puts "Ruby Version #{RUBY_VERSION}"
puts "="*40


[3,4,5,6].each do |factor|
  limit=10**factor
  puts "\nRunning for #{limit} iterations"
  puts "-"*40
  Benchmark.bm do |x|
      x.report("concat:") { bm_concat limit }
      x.report("    <<:") { bm_append limit }
      x.report("  plus:") { bm_plus limit }
      x.report("interp:") { bm_interp limit }
  end
end

Here's the results from a few other rubies on my system:

Ruby Version 2.3.7
========================================

Running for 1000 iterations
----------------------------------------
       user     system      total        real
concat:  0.000000   0.000000   0.000000 (  0.000153)
    <<:  0.000000   0.000000   0.000000 (  0.000127)
  plus:  0.000000   0.010000   0.010000 (  0.002142)
interp:  0.000000   0.000000   0.000000 (  0.003452)

Running for 10000 iterations
----------------------------------------
       user     system      total        real
concat:  0.000000   0.000000   0.000000 (  0.001195)
    <<:  0.010000   0.000000   0.010000 (  0.001109)
  plus:  0.060000   0.050000   0.110000 (  0.117216)
interp:  0.080000   0.070000   0.150000 (  0.146646)

Running for 100000 iterations
----------------------------------------
       user     system      total        real
concat:  0.010000   0.000000   0.010000 (  0.013203)
    <<:  0.010000   0.000000   0.010000 (  0.009131)
  plus:  7.350000   7.670000  15.020000 ( 15.086260)
interp: 14.930000  19.590000  34.520000 ( 34.884925)

JRuby Version 9.1.17.0
========================================

Running for 1000 iterations
----------------------------------------
       user     system      total        real
concat:  0.020000   0.000000   0.020000 (  0.002177)
    <<:  0.000000   0.000000   0.000000 (  0.000965)
  plus:  0.010000   0.000000   0.010000 (  0.002923)
interp:  0.010000   0.000000   0.010000 (  0.002653)

Running for 10000 iterations
----------------------------------------
       user     system      total        real
concat:  0.010000   0.000000   0.010000 (  0.002725)
    <<:  0.010000   0.000000   0.010000 (  0.002532)
  plus:  0.280000   0.050000   0.330000 (  0.118032)
interp:  0.180000   0.000000   0.180000 (  0.078198)

Running for 100000 iterations
----------------------------------------
       user     system      total        real
concat:  0.060000   0.010000   0.070000 (  0.022531)
    <<:  0.050000   0.000000   0.050000 (  0.018783)
  plus:  5.540000   0.140000   5.680000 (  3.591871)
interp:  7.580000   0.100000   7.680000 (  4.379781)

JRuby Version 9.2.5.0
========================================

Running for 1000 iterations
----------------------------------------
       user     system      total        real
concat:  0.010000   0.000000   0.010000 (  0.002389)
    <<:  0.010000   0.000000   0.010000 (  0.002574)
  plus:  0.020000   0.000000   0.020000 (  0.009726)
interp:  0.010000   0.000000   0.010000 (  0.004092)

Running for 10000 iterations
----------------------------------------
       user     system      total        real
concat:  0.000000   0.000000   0.000000 (  0.003194)
    <<:  0.010000   0.000000   0.010000 (  0.003435)
  plus:  0.300000   0.040000   0.340000 (  0.117165)
interp:  0.160000   0.010000   0.170000 (  0.065159)

Running for 100000 iterations
----------------------------------------
       user     system      total        real
concat:  0.060000   0.000000   0.060000 (  0.024769)
    <<:  0.040000   0.000000   0.040000 (  0.012949)
  plus:  5.690000   0.130000   5.820000 (  3.501719)
interp:  7.380000   0.090000   7.470000 (  4.272093)

Ruby Version 2.6.0
========================================

Running for 1000 iterations
----------------------------------------
       user     system      total        real
concat:  0.000112   0.000004   0.000116 (  0.000113)
    <<:  0.000087   0.000003   0.000090 (  0.000089)
  plus:  0.001099   0.000782   0.001881 (  0.001882)
interp:  0.001119   0.000784   0.001903 (  0.001903)

Running for 10000 iterations
----------------------------------------
       user     system      total        real
concat:  0.001496   0.000018   0.001514 (  0.001515)
    <<:  0.001005   0.000005   0.001010 (  0.001010)
  plus:  0.062456   0.049374   0.111830 (  0.112027)
interp:  0.055707   0.043047   0.098754 (  0.099125)

Running for 100000 iterations
----------------------------------------
       user     system      total        real
concat:  0.011245   0.002376   0.013621 (  0.013622)
    <<:  0.009624   0.000204   0.009828 (  0.009873)
  plus:  7.217331   7.229639  14.446970 ( 14.483581)
interp:  7.244183   7.636982  14.881165 ( 15.064573)

surprising - running with --jit in 2.6.0 rc1 has no material effect:

Ruby Version 2.6.0 with --jit
========================================

Running for 1000 iterations
----------------------------------------
       user     system      total        real
concat:  0.000890   0.002269   0.066215 (  0.071825)
    <<:  0.000778   0.002200   0.057190 (  0.063994)
  plus:  0.001852   0.003179   0.073733 (  0.078600)
interp:  0.001985   0.003323   0.074870 (  0.081532)

Running for 10000 iterations
----------------------------------------
       user     system      total        real
concat:  0.002784   0.000990   0.003774 (  0.003724)
    <<:  0.001227   0.000192   0.001419 (  0.001416)
  plus:  0.061307   0.045535   0.106842 (  0.107475)
interp:  0.055429   0.042227   0.097656 (  0.097842)

Running for 100000 iterations
----------------------------------------
       user     system      total        real
concat:  0.011286   0.002768   0.014054 (  0.014086)
    <<:  0.009409   0.000219   0.009628 (  0.009694)
  plus:  7.252654   7.438926  14.691580 ( 14.733727)
interp:  7.240729   7.408015  14.648744 ( 14.706172)

```
over 1 year ago ·