Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ruby Array concat versus + speed?

I did small performance test of Ruby's array concat() vs + operation and concat() was way too fast.

I however am not clear on why concat() is so fast?

Can anyone help here?

This is the code I used:

t = Time.now
ar = []
for i in 1..10000
ar = ar + [4,5]
end
puts "Time for + " + (Time.now - t).to_s 


t = Time.now
ar = []
for i in 1..10000
ar.concat([4,5])
end
puts "Time for concat " + (Time.now - t).to_s 
like image 587
snow_leopard Avatar asked May 20 '14 14:05

snow_leopard


5 Answers

According to the Ruby docs, the difference is:

Array#+ :

Concatenation — Returns a new array built by concatenating the two arrays together to produce a third array.

Array#concat :

Array#concat : Appends the elements of other_ary to self.

So the + operator will create a new array each time it is called (which is expensive), while concat only appends the new element.

like image 125
zwippie Avatar answered Nov 08 '22 06:11

zwippie


The answer lies in Ruby's underlying C implementation of the + operator and the concat methods.

Array#+

rb_ary_plus(VALUE x, VALUE y)
{
    VALUE z;
    long len, xlen, ylen;

    y = to_ary(y);
    xlen = RARRAY_LEN(x);
    ylen = RARRAY_LEN(y);
    len = xlen + ylen;
    z = rb_ary_new2(len);

    ary_memcpy(z, 0, xlen, RARRAY_CONST_PTR(x));
    ary_memcpy(z, xlen, ylen, RARRAY_CONST_PTR(y));
    ARY_SET_LEN(z, len);
    return z;
}

Array#concat

rb_ary_concat(VALUE x, VALUE y)
{
    rb_ary_modify_check(x);
    y = to_ary(y);
    if (RARRAY_LEN(y) > 0) {
        rb_ary_splice(x, RARRAY_LEN(x), 0, y);
    }
    return x;
}

As you can see, the + operator is copying the memory from each array, then creating and returning a third array with the contents of both. The concat method is simply splicing the new array into the original one.

like image 32
Chris Cashwell Avatar answered Nov 08 '22 06:11

Chris Cashwell


If you're going to run benchmarks, take advantage of prebuilt tools and reduce the test to the minimum necessary to test what you want to know.

Starting with Fruity, which provides a lot of intelligence to its benchmarking:

require 'fruity'

compare do
  plus { [] + [4, 5] }
  concat { [].concat([4, 5]) }
end
# >> Running each test 32768 times. Test will take about 1 second.
# >> plus is similar to concat

When things are close enough to not really worry about, Fruity will tell us they're "similar".

At that point Ruby's built-in Benchmark class can help:

require 'benchmark'

N = 10_000_000
3.times do
  Benchmark.bm do |b|
    b.report('plus')  { N.times { [] + [4, 5] }}
    b.report('concat') { N.times { [].concat([4,5]) }}
  end
end
# >>        user     system      total        real
# >> plus  1.610000   0.000000   1.610000 (  1.604636)
# >> concat  1.660000   0.000000   1.660000 (  1.668227)
# >>        user     system      total        real
# >> plus  1.600000   0.000000   1.600000 (  1.598551)
# >> concat  1.690000   0.000000   1.690000 (  1.682336)
# >>        user     system      total        real
# >> plus  1.590000   0.000000   1.590000 (  1.593757)
# >> concat  1.680000   0.000000   1.680000 (  1.684128)

Notice the different times. Running a test once can result in misleading results, so run them several times. Also, make sure your loops result in a time that isn't buried in background noise caused by processes kicking off.

like image 33
the Tin Man Avatar answered Nov 08 '22 05:11

the Tin Man


The OP's question, as noted in other answers, is comparing two operators that perform different purposes. One, concat, which is destructive to (mutates) the original array, and + which is non-destructive (pure functional, no mutation).

I came here looking for a more comparable test, not realizing at the time, that concat was destructive. In case it's useful to others looking to compare two purely functional, non-destructive operations, here is a benchmark of array addition (array1 + array2) vs array expansion ([*array1, *array2]). Both, as far as I'm aware, result in 3 arrays being created: 2 input arrays, 1 new resulting array.

Hint: + wins.

Code

# a1 is a function producing a random array to avoid caching
a1 = ->(){ [rand(10)] }
a2 = [1,2,3]
n = 10_000_000
Benchmark.bm do |b|
  b.report('expand'){ n.times{ [*a1[], *a2] } }
  b.report('add'){ n.times{ a1[]+a2 } }
end

Result

user     system      total        real
expand  9.970000   0.170000  10.140000 ( 10.151718)
add  7.760000   0.020000   7.780000 (  7.792146)
like image 3
Chad M Avatar answered Nov 08 '22 06:11

Chad M


I did benchmark using two versions of ruby. And the result shows concat is faster than plus(+)

require 'benchmark'

N = 10_000_000
5.times do
  Benchmark.bm do |b|
    b.report('concat') { N.times { [].concat([4,5]) }}
    b.report('plus')  { N.times { [] + [4, 5] }}
  end
end

ruby-2.5.3

       user     system      total        real
concat  1.347328   0.001125   1.348453 (  1.349277)
plus  1.405046   0.000110   1.405156 (  1.405682)
       user     system      total        real
concat  1.263601   0.012012   1.275613 (  1.276105)
plus  1.336407   0.000051   1.336458 (  1.336951)
       user     system      total        real
concat  1.264517   0.019985   1.284502 (  1.285004)
plus  1.329239   0.000002   1.329241 (  1.329733)
       user     system      total        real
concat  1.347648   0.004012   1.351660 (  1.352149)
plus  1.821616   0.000034   1.821650 (  1.822307)
       user     system      total        real
concat  1.256387   0.000000   1.256387 (  1.256828)
plus  1.269306   0.007997   1.277303 (  1.277754)

ruby-2.7.1

       user     system      total        real
concat  1.406091   0.000476   1.406567 (  1.406721)
plus  1.295966   0.000044   1.296010 (  1.296153)
       user     system      total        real
concat  1.281295   0.000000   1.281295 (  1.281436)
plus  1.267036   0.000027   1.267063 (  1.267197)
       user     system      total        real
concat  1.291685   0.000003   1.291688 (  1.291826)
plus  1.266182   0.000000   1.266182 (  1.266320)
       user     system      total        real
concat  1.272261   0.000001   1.272262 (  1.272394)
plus  1.265784   0.000000   1.265784 (  1.265916)
       user     system      total        real
concat  1.272507   0.000001   1.272508 (  1.272646)
plus  1.294839   0.000000   1.294839 (  1.294974)

Memory Usage

require "benchmark/memory"

N = 10_000_00
Benchmark.memory do |x|
  x.report("array concat") { N.times { [].concat([4,5]) } }
  x.report("array +") { N.times { [] + [4, 5] } }

  x.compare!
end

Calculating -------------------------------------
        array concat    80.000M memsize (     0.000  retained)
                         2.000M objects (     0.000  retained)
                         0.000  strings (     0.000  retained)
             array +   120.000M memsize (     0.000  retained)
                         3.000M objects (     0.000  retained)
                         0.000  strings (     0.000  retained)

Comparison:
        array concat:   80000000 allocated
             array +:  120000000 allocated - 1.50x more
like image 2
Aniket Tiwari Avatar answered Nov 08 '22 06:11

Aniket Tiwari