Exceptions and performance

I’m trying to understand the performance cost of exceptions. Here is a function reading lines from stdin and returning a list in reverse order:

(*using options*)
let input_rev () =
  let rec aux acc =
    match In_channel.input_line stdin with
    | None -> acc
    | Some s -> aux (s :: acc)
  in aux []
  
(*using exceptions*)
let input_rev' () =
  let rec aux acc =
    try aux @@ read_line () :: acc
    with End_of_file -> acc
  in aux []

Is input_rev'slower than input_rev? I feel like the recursive nesting of ‘try..with’ blocks smells a little bit, but maybe it’s fine. (I’m talking about performance only, not the overall merits of using options.)

If it is slower, is there a way to rewrite input_rev'without nesting ‘try…with’ blocks and without using mutation?

Last question: is my version of input_rev'tail-recursive? Or does the ‘try’ before the function call break tail-recursion?

input_rev’ is not tail recursive, you would need to write it as

let input_rev' () =
  let rec aux acc =
    match read_line () with 
    | v -> aux @@ v :: acc
    | exception End_of_file -> acc
  in aux []

to be tail recursive
(equivalently match read_line () :: acc with v -> aux v | exception End_of_file -> acc)

Note that In_channel.input_line is a wrapper around Stdlib.input_line defined as

let input_line ic =
  match Stdlib.input_line ic with
  | s -> Some s
  | exception End_of_file -> None

so comparing using it vs calling Stdlib.input_line directly is not quite sensible.
Also Stdlib.read_line does flush stdout on top of input_line stdin.

I agree with @SkySkimmer that you should make input_rev' tail recursive, and use Stdlib.input_line stdin instead of read_line () to avoid the extra “flush” performed by read_line.

Once you’ve done that, I think input_rev and input_rev' should be about as fast. Try doing some timings if you’d like to be sure.

Here is a version of input_rev that installs a single exception handler instead of one handler per line:

let input_rev () =
  let res = ref [] in
  begin try
    while true do
      res := input_line stdin :: !res
    done
  with End_of_file ->
    ()
  end;
  !res

Exception handling is cheap in OCaml, and I/O is always expensive, so I wouldn’t expect this code to make much of a speed difference.