Ruby Array concat против + скорости?
Я выполнил небольшой тест производительности работы с массивом Ruby concat()
vs +
, а concat()
был слишком быстрым.
Я не знаю, почему concat()
так быстро?
Может ли кто-нибудь помочь здесь?
Это код, который я использовал:
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
Ответы
Ответ 1
В соответствии с Ruby docs разница заключается в следующем:
Массив # +:
Конкатенация - возвращает новый массив, созданный объединением двух массивов вместе для создания третьего массива.
Массив # concat:
Array # concat: добавляет элементы other_ary к себе.
Таким образом, оператор +
будет создавать новый массив каждый раз, когда он вызывается (что дорого), а concat
добавляет только новый элемент.
Ответ 2
Ответ лежит на Ruby, лежащем в основе реализации C оператора +
и методов concat
.
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;
}
Как вы можете видеть, оператор +
копирует память из каждого массива, а затем создает и возвращает третий массив с содержимым обоих. Метод concat
просто соединяет новый массив с исходным.
Ответ 3
Если вы собираетесь запускать тесты, используйте заранее подготовленные инструменты и уменьшите тест до минимума, необходимого для проверки того, что вы хотите знать.
Начиная с Fruity, который обеспечивает большой интеллект для его бенчмаркинга:
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
Когда все будет достаточно близко, чтобы не волноваться, Фрутис скажет нам, что они "похожи".
В этот момент Ruby встроенный Benchmark класс может помочь:
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)
Обратите внимание на разное время. Запуск теста один раз может привести к вводящим в заблуждение результатам, поэтому запустите их несколько раз. Кроме того, убедитесь, что ваши петли приводят к тому, что время не затухает в фоновом шуме, вызванном запуском процессов.
Ответ 4
Вопрос OP, как указано в других ответах, сравнивает двух операторов, которые выполняют разные цели. Один, concat
, который разрушает исходный массив (мутирует) и +
, который является неразрушающим (чистый функционал, без мутации).
Я пришел сюда, чтобы найти более сопоставимый тест, не понимая в то время, что concat был разрушительным. В случае, если это полезно для других, которые хотят сравнить две чисто функциональные, неразрушающие операции, здесь приведен пример добавления массива (array1 + array2
) и расширения массива ([*array1, *array2]
). Оба, насколько мне известно, приводят к созданию 3 создаваемых массивов: 2 массива ввода, 1 новый результирующий массив.
Подсказка: +
побеждает.
код
# 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
Результат
user system total real
expand 9.970000 0.170000 10.140000 ( 10.151718)
add 7.760000 0.020000 7.780000 ( 7.792146)