Some experiments with stack sizes:
function test_recursion_depth(depth::Int, args...)
depth >= 1000000000000 && return nothing
print("\33[2K\rHello from depth $(depth)")
test_recursion_depth(depth + 1, args...)
print("$(depth) $(args)") # Just to prevent LLVM removing args
end
So with no arguments, we can get up to a call depth of 86660:
julia> test_recursion_depth(0)
Hello from depth 86660ERROR: StackOverflowError:
Let’s see if we have 10 arguments:
julia> test_recursion_depth(0, ones(10)...)
Hello from depth 12998
meaning we hit a stack overflow at about 13,000 call depth.
And, for 100 arguments:
julia> test_recursion_depth(0, ones(100)...)
Hello from depth 6117
Does the type of the argument affect the stack frame size?
julia> test_recursion_depth(0, ones(BigInt, 100)...)
Hello from depth 6117
julia> test_recursion_depth(0, ntuple(_ -> ones(10), 100)...)
Hello from depth 6117
Nope, just the number of arguments. What about kwargs?
julia> function test_recursion_depth_kws(depth::Int; kws...)
depth >= 1000000000000 && return nothing
print("\33[2K\rHello from depth $(depth)")
test_recursion_depth_kws(depth + 1; kws...)
print("$(depth) $(kws)")
end
julia> test_recursion_depth_kws(0; NamedTuple(Symbol("x$i") => 1 for i in 1:100)...)
Hello from depth 2511
The all-keywords example seems to have a ~2.4x larger stack frame size.
What about when you have local arguments within the function body?
julia> function test_recursion_depth_locals(::Val{n}, depth::Int, args...) where {n}
depth >= 1000000000000 && return nothing
print("\33[2K\rHello from depth $(depth)")
a = ntuple(_ -> rand(), Val(n))
test_recursion_depth_locals(Val(n), depth + 1, args...)
print("$(depth) $(a) $(args)") # Just to prevent LLVM removing args
end;
Let’s see 1 value, 50 values, and 500 values in a tuple, in the function body:
julia> test_recursion_depth_locals(Val(1), 0, ones(10)...)
Hello from depth 12091
julia> test_recursion_depth_locals(Val(50), 0, ones(10)...)
Hello from depth 7646
julia> test_recursion_depth_locals(Val(500), 0, ones(10)...)
Hello from depth 1710
So the function body seems to affect the stack frame size quite a bit.
Okay, now lets try with different stack sizes via Task(f, n)
(still undocumented, but seems to work pretty well) –
1 MB
julia> fetch(schedule(Task(() -> test_recursion_depth(0, ones(10)...), 1024 * 1024)))
Hello from depth 1532
4 MB (default for a task)
julia> fetch(schedule(Task(() -> test_recursion_depth(0, ones(10)...), 4 * 1024 * 1024)))
Hello from depth 6447
julia> fetch(schedule(Task(() -> test_recursion_depth(0, ones(10)...))))
Hello from depth 6447
8 MB (seems to be default for root task)
julia> fetch(schedule(Task(() -> test_recursion_depth(0, ones(10)...), 8 * 1024 * 1024)))
Hello from depth 13001
Note: it can get up a bit higher than the initial test – because the task has a fresh stack, whereas the root task has the REPL in its stack.
Now, 64 MB:
julia> fetch(schedule(Task(() -> test_recursion_depth(0, ones(10)...), 64 * 1024 * 1024)))
Hello from depth 104751
Or 1 GB:
julia> fetch(schedule(Task(() -> test_recursion_depth(0, ones(10)...), 1024 * 1024 * 1024)))
Hello from depth 1677615
(Note: it’s just a stack size – that memory is virtual. Only if you use that stack size do you actually allocate it.)