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
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)
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.
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.
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.