Fatal error: exception Stack_overflow from ocamlrun only under production-size inputs
0 reputation · 12 Jan 2025, 13:06 UTC
0 reputation · 12 Jan 2025, 13:06 UTC
An OCaml program compiled to bytecode with ocamlc and run under ocamlrun terminates with Fatal error: exception Stack_overflow when processing production-scale data. The same binary handles the smaller inputs used in local testing without any failure.
The failing code path is a recursive function over a large list (on the order of hundreds of thousands of elements). The function is not written in tail-recursive style, so each recursive call consumes C stack space in the bytecode interpreter. Local test data stays well below the default stack limit, which is why the overflow only surfaces with production input sizes.
The constraint is that the production deployment must keep using the bytecode runtime for now, and the input size cannot be capped. Two options are on the table: rewriting the recursion to be tail-recursive (possibly with an accumulator and a final List.rev), or raising the stack limit available to ocamlrun. Each has portability and memory trade-offs that are not fully clear.
OCAMLRUNPARAM l setting) a reliable production fix, or does it only postpone the overflow for larger inputs?ocamlopt) builds enough that switching the production target would change the failure threshold?32775 reputation · 12 Jan 2025, 14:21 UTC
OCAMLRUNPARAM l is not a reliable production fix. It only postpones the overflow. The bytecode interpreter checks its internal guard at each function entry, but each recursive call still consumes C stack frames. Once the OS thread stack limit (ulimit -s, typically 8 MiB on Linux) is reached, the process receives SIGSEGV instead of a clean Stack_overflow exception, making debugging harder.ocamlopt) does not solve non-tail recursion. The default OS stack limit is higher (8 MiB vs. the bytecode default of 2 MiB), so the failure threshold moves, but the fundamental linear stack growth remains. Only tail-call optimization—which ocamlopt performs aggressively—eliminates the growth, and that requires the function to be written in tail-recursive form.The bytecode interpreter executes every recursive call as a C function call. A list of 300,000 elements means 300,000 C stack frames. The default OCAMLRUNPARAM l=256k (256 kilowords = 2 MiB on 64-bit) allows roughly 25,000–50,000 frames depending on frame size—far below production scale. Raising l to, say, 1000k (8 MiB) may cover the current input, but:
l to the OS RLIMIT_STACK at startup without logging.ulimit -s unlimited or a higher value), the process segfaults.A tail-recursive rewrite with an accumulator and a final List.rev reduces C stack usage to O(1). The temporary list created by List.rev doubles heap allocation for that pass, so ensure OCAMLRUNPARAM h (heap size) accommodates it.
ulimit -s (value in kilobytes). Confirm OCAMLRUNPARAM l * 8 < ulimit -s * 1024.OCAMLRUNPARAM l=256k,v=0x400 ./your_program Observe the reported stack size at startup.Stack_overflow, then binary-search the maximum depth for the current l value. Compare that depth with your production input size.ocamlopt and run without OCAMLRUNPARAM. If it still overflows, the recursion is not tail-recursive in native code either—confirming the rewrite is necessary regardless of backend.What is the exact recursion depth (list length) in the largest production input? If it exceeds ~50,000, the bytecode default cannot handle it even with a raised l unless the OS limit is also increased, and the segfault risk remains. This number confirms whether a configuration-only approach could ever be viable.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.