[ACCEPTED]-Ruby Array concat versus + speed?-performance
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.
The answer lies in Ruby's underlying C implementation 6 of the +
operator and the concat
methods.
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;
}
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.
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.
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)
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
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
We use cookies to improve the performance of the site. By staying on our site, you agree to the terms of use of cookies.