[ACCEPTED]-Ruby Array concat versus + speed?-performance

Accepted answer
Score: 47

According to the Ruby docs, the difference is:

Array#+ :

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

Array#concat :

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

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

Score: 19

The answer lies in Ruby's underlying C implementation 6 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 5 can see, the + operator is copying the memory 4 from each array, then creating and returning 3 a third array with the contents of both. The 2 concat method is simply splicing the new array 1 into the original one.

Score: 8

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

Starting with Fruity, which provides 10 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 9 things are close enough to not really worry 8 about, Fruity will tell us they're "similar".

At 7 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 6 the different times. Running a test once 5 can result in misleading results, so run 4 them several times. Also, make sure your 3 loops result in a time that isn't buried 2 in background noise caused by processes 1 kicking off.

Score: 2

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

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

I did benchmark using two versions of ruby. And 2 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 1 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
Score: 0

Interestingly, in benchmarking the differences 5 between 3 mutating approaches for combining arrays 4 my benchmarks indicate that the add and 3 reassign approach is in fact the fastest 2 with a constant margin of approximately 1 1%. (with ruby 3.1.2)

Benchmark:

A = [4, 5].freeze
N = 10_000_000

require 'benchmark'
Benchmark.bm do |b|
  b.report('plus  ') { N.times { c = [1, 2]; c = c + A } }
  b.report('concat') { N.times { [1, 2].concat(A) } }
  b.report('push  ') { N.times { [1, 2].push(*A) } }
end

Results:

       user     system      total        real
plus    1.180429   0.015026   1.195455 (  1.199552)
concat  1.228267   0.008172   1.236439 (  1.237004)
push    1.242709   0.007759   1.250468 (  1.251412)

More Related questions