Let’s say I’m using some recursive algorithm which risks a stack overflow, if I can detect that and deal with it somehow then I’d prefer that over a crash. Is it possible?
This isn’t Odin-specific, but it’s my understanding that you can always convert an algorithm with recursive procedure calls into a single procedure call that uses an explicit stack (think Depth First Search implementations that use The Stack vs. an explicit stack). That would give you visibility into what you allocate and you can bail when you see that you exceed the bounds of the stack.
It would be super cool if there was a context.stack_allocator that you could override with your own tracking stack allocator and existing stack allocations just worked… although I’m not sure if that’s even possible to implement.
I have a solution that kinda works but it’s also kind of hacky. It probably won’t work on all platforms.
package recurse_test
import "core:fmt"
stack_ptr_begin: rawptr
get_used_stack_mem :: #force_no_inline proc "c" () -> int {
current: int
// stack may grow downwards or upwards, and downwards is more common so abs in needed
return abs(int(uintptr(¤t) - uintptr(stack_ptr_begin)))
}
main :: proc() {
start: int
stack_ptr_begin = &start
func()
}
ptr: rawptr
func :: proc() {
@(static) i: int
i += 1
a, b: [64]uint
c := a + b
// to avoid optimizing out c
ptr = &c
used := get_used_stack_mem()
fmt.println(used)
if used < (1 << 19) {
func()
} else {
fmt.println("out of memory at iteration", i)
}
}
Why not just use tail call optimization to reuse the same stack frame?
That was just an example use case. There are other use cases for knowing the used memory on a stack. Like allocating space for fibers, although I’m not yet sure if it’s possible to implement in Odin.
Is there any way to know how much memory has been allocated on the stack?
– There is.
– Probably.
– This code snippet is misleading.
Here is my working example (took me 5 days of trial and error).
Notes:
It's not portable
You have to use the OS-specific procedures to get the stack info
It's not multithreaded
It’s done in the main thread
The thread's stack space is assumed to be used by the recursive procedure only
This assumption is wrong, so I used ~50kb as a safety margin
Windows version
package main
import "core:fmt"
foreign import lib {
"kernel32.lib",
}
foreign lib {
GetCurrentThreadStackLimits :: proc "c" (LowLimit: ^u64, HighLimit: ^u64) ---
}
rec_proc :: proc(p_dyn_arr: ^[dynamic]int, p_marker: ^int) {
marker: int
// your calculations
a, b: [64]uint
c := a + b
// If the work isn't done:
// 1) calculate the usage
size_used: int
switch len(p_dyn_arr) {
// If it's the first call - assume the size_used is zero
case 0:
// If it's the second call - correct the first call usage value
case 1:
p_dyn_arr[0] = abs(int(uintptr(p_marker) - uintptr(&marker)))
size_used = p_dyn_arr[0] * 2
// If it's the n-th call - the size_used is n times bigger
case:
size_used = p_dyn_arr[0] * (len(p_dyn_arr) + 1)
}
append(p_dyn_arr, size_used)
// 2) make another call if possible
dif := 0
if len(p_dyn_arr) > 1 {
dif = size_used - p_dyn_arr[len(p_dyn_arr) - 2]
}
if (size_used + dif < cap(p_dyn_arr)) {
fmt.printfln("calls made : %v; size used: %v",
len(p_dyn_arr), size_used)
rec_proc(p_dyn_arr, &marker)
} else {
fmt.printfln("Out of memory after %v call(s).", len(p_dyn_arr))
}
}
main :: proc() {
// Get the stack info
low_limit, high_limit: u64
GetCurrentThreadStackLimits(&low_limit, &high_limit)
// ~50kb is used as a safety margin
da := make([dynamic]int, 0, (high_limit - low_limit) - 50_000)
defer delete(da)
rec_proc(&da, nil)
}
Linux version
package main
import "core:fmt"
import "core:sys/linux"
rec_proc :: proc(p_dyn_arr: ^[dynamic]int, p_marker: ^int) {
marker: int
// your calculations
a, b: [64]uint
c := a + b
// If the work isn't done:
// 1) calculate the usage
size_used: int
switch len(p_dyn_arr) {
// If it's the first call - assume the size_used is zero
case 0:
// If it's the second call - correct the first call usage value
case 1:
p_dyn_arr[0] = abs(int(uintptr(p_marker) - uintptr(&marker)))
size_used = p_dyn_arr[0] * 2
// If it's the n-th call - the size_used is n times bigger
case:
size_used = p_dyn_arr[0] * (len(p_dyn_arr) + 1)
}
append(p_dyn_arr, size_used)
// 2) make another call if possible
dif := 0
if len(p_dyn_arr) > 1 {
dif = size_used - p_dyn_arr[len(p_dyn_arr) - 2]
}
if (size_used + dif < cap(p_dyn_arr)) {
fmt.printfln("calls made : %v; size used: %v",
len(p_dyn_arr), size_used)
rec_proc(p_dyn_arr, &marker)
} else {
fmt.printfln("Out of memory after %v call(s).", len(p_dyn_arr))
}
}
main :: proc() {
// Get the stack info
rlm: linux.RLimit
linux.getrlimit(linux.RLimit_Kind.STACK, &rlm)
// ~50kb is used as a safety margin
da := make([dynamic]int, 0, rlm.cur - 50_000)
defer delete(da)
rec_proc(&da, nil)
}