Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SystemStackError when pushing more than 130798 objects into an array

Tags:

ruby

I am trying to understand why pushing many (in my case 130798) objects in an array returns a SystemStackError.

big = Array.new(130797, 1)
[].push(*big) && false
=> false

bigger = Array.new(130798, 1)
[].push(*bigger) && false
=> SystemStackError: stack level too deep
     from (irb):104
     from /Users/julien/.rbenv/versions/2.2.0/bin/irb:11:in `<main>'

I was able to reproduce it on MRI 1.9.3 and 2.2.0 while no errors were raised on Rubinius (2.5.2).

I understand this is due to the way Array are implemented in MRI but don't quite understand why a SystemStackError is raised.

like image 369
jgoyon Avatar asked Dec 24 '22 22:12

jgoyon


1 Answers

Ruby's error message ("stack level too deep") isn't accurate here - what Ruby is really saying is "I ran out of stack memory", which is usually caused by infinite recursion, but in this case, is caused by you passing more arguments than Ruby has memory allocated to handle.

Ruby 2.0+ has a maximum stack size controlled by RUBY_THREAD_VM_STACK_SIZE (prior to 2.0 this was controlled by the C limits, set via ulimit). Each argument passed to a method gets pushed onto the thread's stack; if you push more arguments onto the stack than RUBY_THREAD_VM_STACK_SIZE has room to accomodate, you'll get a SystemStackError. You can see this limit from IRB:

RubyVM::DEFAULT_PARAMS[:thread_vm_stack_size]
=> 1048576

By default, each thread has 1MB of stack it can use. Ruby Fixnums are 8 bytes large, and on my system, I overflow at 130808 arguments, or 1046464 bytes allocated, leaving 2112 bytes allocated for the rest of the call stack. By using the splat operator (*) you are saying "take this list of 130798 Fixnums and expand it into 130798 arguments to be passed on the stack"; you simply don't have enough stack memory allocated to hold them all.

If you need to, you can increase RUBY_THREAD_VM_STACK_SIZE when you invoke Ruby:

$ RUBY_THREAD_VM_STACK_SIZE=2097152 irb
> [].push(*Array.new(150808, 1)); nil
 => nil

And this will increase the number of arguments you can pass. However, it also means that each thread will allocate twice as much stack, which is probably not desirable. You should also note that Fibers have a separate stack allocation setting, which is typically substantially smaller, since Fibers are designed to by lightweight and disposable.

Very rarely should you ever need to pass that much data on the stack; typically, if you need to pass a large amount of data to a method, you would pass an object as an argument (that is, on the stack, such as a Hash or Array) whose storage is allocated on the heap, so your stack usage is measured in bytes even if your heap usage is measured in megabytes. That is, you would pass your very large array to your method (which could hold gigabytes of data on the heap without issue), then you would iterate that array in your method.

like image 109
Chris Heald Avatar answered Feb 15 '23 23:02

Chris Heald