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
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.
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.
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 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)
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With