Массив # push вызывает ошибку слишком высокого уровня стека с большими массивами
Я сделал два массива, каждый с 1 миллионом предметов:
a1 = 1_000_000.times.to_a
a2 = a1.clone
Я попытался нажать a2 в a1:
a1.push *a2
Это возвращает SystemStackError: stack level too deep
.
Однако, когда я пытаюсь выполнить concat
, я не получаю ошибку:
a1.concat a2
a1.length # => 2_000_000
Я также не получаю ошибку с оператором splat:
a3 = [*a1, *a2]
a3.length # => 2_000_000
Почему это так? Я посмотрел документацию на Array#push
, и он написан на C. Я подозреваю, что он может делать некоторую рекурсию под капотом и почему он вызывает эту ошибку для больших массивов. Это правильно? Разве не рекомендуется использовать push
для больших массивов?
Ответы
Ответ 1
Я думаю, что это не ошибка рекурсии, а ошибка стека аргументов. Вы используете лимит глубины стека Ruby VM для аргументов.
Проблема - это оператор splat, который передается как аргумент для push
. Оператор splat расширяется до миллиона элементов списка аргументов для push
.
Поскольку аргументы функции передаются как элементы стека, а предварительно сконфигурированный максимальный размер размера стека Ruby VM:
RubyVM::DEFAULT_PARAMS[:thread_vm_stack_size]
=> 1048576
Это именно тот предел.
Вы можете попробовать следующее:
RUBY_THREAD_VM_STACK_SIZE=10000000 ruby array_script.rb
.. и он будет работать нормально.
Это также причина, по которой вы хотите использовать concat
, поскольку весь массив можно передать как одну ссылку, а concat
будет обрабатывать массив внутри. В отличие от push
+ splat, который попытается использовать стек как временное хранилище для всех элементов массива.
Ответ 2
Каспер уже ответил на вопрос в названии и дал вам решение, которое вы можете использовать для создания a1.push *a2
, но я хотел бы поговорить о последнем вопросе, который вы задали, о том, хорошая ли это идея.
Более конкретно, если вы собираетесь работать с массивами, которые составляют миллионы элементов в производственном коде, производительность становится чем-то, что нужно иметь в виду. http://www.continuousthinking.com/2011/09/07/ruby_array_plus_vs_push.html содержит 4 различных способа обработки конкатенации массива в ruby: +
, .push
, <<
и .concat
.
Там они упоминают, что array.push
будет эффективно обрабатывать каждый аргумент отдельно и увеличивать размер массива на 50% каждый раз, когда массив слишком мал. Это означает, что в вашем примере a
будет увеличен в размере 2 раза и получит 1 миллион приложений. Между тем, array.concat
сначала вычислит новый размер массива, расширит исходный массив и затем скопирует новый массив в нужное место.
Для ситуаций, подобных вашим, concat
, скорее всего, будет более результативным, как из памяти, так и с точки зрения использования процессора. Однако без контрольных показателей я не могу сказать точно. Моя рекомендация - измерять время и использование памяти для выполнения обеих операций для размера массивов, которые вы хотите обработать. concat
, скорее всего, выйдет на первое место, но я могу ошибаться на этом фронте.