【问题标题】:Array#push causes "stack level too deep" error with large arraysArray#push 导致大型数组出现“堆栈级别太深”错误
【发布时间】:2018-08-23 23:17:49
【问题描述】:

我做了两个数组,每个数组有 100 万个项目:

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 不是一个好主意吗?

【问题讨论】:

  • 我没有确切的答案,但总的来说,我会说传递非常大的参数列表(其中“超过”定义为几百个以上)往往会导致奇怪的行为,因为参数列表通常存储在内存中的某个位置,它会假设它们的大小。有些语言甚至明确定义了最大参数计数;例如,在 Common Lisp 中,变量 CALL-ARGUMENTS-LIMIT 告诉您参数的最大数量。

标签: arrays ruby


【解决方案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 不同,后者会尝试将堆栈用作所有数组元素的临时存储。

【讨论】:

  • 但是为什么[*a1] 使用默认堆栈大小?通话是否正在优化?
  • 因为不是调用,而是文字。调用堆栈上的参数列表不会溢出,因为没有参数,因为没有调用。
  • @JörgWMittag 有道理,不知何故。我预计它会以同样的方式失败,因为 - 尽管没有调用 Ruby 方法 - 必须调用底层 C 代码来创建和填充 a1 元素的数组。
  • 是的,但可能只有一个参数,而不是一百万。
【解决方案2】:

Casper 已经回答了标题中的问题,并为您提供了可用于使 a1.push *a2 工作的解决方案,但我想谈谈您提出的最后一个问题,关于这是否是个好主意。

更具体地说,如果您要在生产代码中使用数百万个项目的数组,则需要牢记性能。 http://www.continuousthinking.com/2011/09/07/ruby_array_plus_vs_push.html 概述了在 ruby​​ 中处理数组连接的 4 种不同方法:+.push<<.concat

他们提到array.push 将有效地分别处理每个参数,并在每次数组太小时时将数组大小增加 50%。这意味着在您的示例中,a 的大小将增加 2 倍并获得 100 万个追加。同时array.concat会先计算新数组的大小,扩展原数组,然后将新数组复制到正确的位置。

对于像您这样的情况,concat 很可能会更高效,无论是从内存还是从 CPU 使用的角度来看。但是,如果没有基准,我不能肯定地说。我的建议是测量时间和内存使用情况,以针对要处理的数组大小执行这两项操作。 concat 很可能会名列前茅,但我可能在这方面弄错了。

【讨论】:

猜你喜欢
  • 1970-01-01
  • 2012-04-10
  • 1970-01-01
  • 2023-04-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-10-21
相关资源
最近更新 更多