Массив # 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, скорее всего, выйдет на первое место, но я могу ошибаться на этом фронте.