TL;DR it appears that @silene is correct, that OCaml does record the backtrace (and pays a cost proportional to stack-depth) but this is only a small part of the cost of “materializing the backtrace” into the heap (which would need to be done to carry it around in a Result).
First, thank you for pointing this out: my understanding was based on seeing that exception backtraces aren’t reliably recorded, and the only way to really be sure of getting one, is to ask for it right after the exception is caught the first time.
But second [now, digging thru the source … ah, isn’t the source a dream to read thru?] I find that there are two steps: (1) copy the backtrace from the stack-frames into a “backtrace buffer” (which appears to be associated with the global state (and hence apparently static ?), and (2) copying that into a heap-allocated array for the application to use or discard, e.g. for adding to a Result.
I wrote a little test to check this out. I’ll insert the results, followed by the test and makefile. It’s possible I’ve done something wrong. The test:
(1) call record_backtrace
(2) with a flag, either calls get_backtrace() or does not, at the point it catches the exception
(3) and then K times, does a depth-N recursion, where it does a try/with block – within which it does a depth-M recursion, and inside that it does a failwith.
(4) so as we vary N, we are varying the depth of the stack
(5) varying M varies the depth of the stack between the try-catch and the raise (not as interesting)
(6) and we can control whether get_backtrace gets called to materialize the backtrace to the heap.
(7) the test is run as test1 <get-backtrace> <n> <m> <k>
It seems that indeed, the cost of a raise/catch is proportional to the depth of the stack. But also that get_backtrace
adds 10x to that cost.
Results:
./test1 false run1 1000 10 100
run1@100: 0.000066
./test1 true run1 1000 10 100
run1@100: 0.052373
./test1 false run1 10000 10 100
run1@100: 0.000741
./test1 true run1 10000 10 100
run1@100: 0.054388
./test1 false run1 100000 10 100
run1@100: 0.006881
./test1 true run1 100000 10 100
run1@100: 0.077785
source (e1.ml)
Printexc.record_backtrace true ;;
let get_backtrace = ref true ;;
let depth pre f post n =
let rec drec n =
pre () ;
let rv =
if n = 0 then f() else drec (n-1)
in post () ; rv
in drec n
;;
let raiser () =
failwith "caught" ;;
let catcher pre post k =
try
depth pre raiser post k
with Failure _ ->
if !get_backtrace then
ignore(Printexc.get_backtrace())
;;
let nop () = () ;;
let harness ~tag reps f =
let stime = Unix.gettimeofday() in
let () = f() in
let etime = Unix.gettimeofday () in
Fmt.(pf stdout "%s@%d: %f\n%!" tag reps (etime -. stime))
;;
let bt = bool_of_string Sys.argv.(1) in
let tag = Sys.argv.(2) in
let n = int_of_string Sys.argv.(3) in
let m = int_of_string Sys.argv.(4) in
let reps = int_of_string Sys.argv.(5) in
get_backtrace := bt ;
harness ~tag:tag reps (fun () -> depth nop (fun () -> catcher nop nop m) nop n)
;;
Makefile
test:: all
./test1 false run1 1000 10 100
./test1 true run1 1000 10 100
./test1 false run1 10000 10 100
./test1 true run1 10000 10 100
./test1 false run1 100000 10 100
./test1 true run1 100000 10 100
all: test1
test1: e1.ml
ocamlfind ocamlc -g -package fmt,unix -linkall -linkpkg -o test1 e1.ml