Thank you for your answers.
Pierre Chopin <pierre@punchup.com> writes:I find that in most cases speed is not a major concern and then it comes
> Hi,
>
> I benchmarked two programs, in one case the main function throw an exception
> that is caught, in the other the function returns an option that is pattern
> matched on.
>
> I noticed that, whether the exception is thrown or not, the option version is
> always faster.
>
> Is there any case where it makes sense, performance wise, to use exception
> instead of 'a option ?
down to taste and readability of the code.
0.34s user 0.01s system 99% cpu 0.351 total
> test1.ml
> ----------------------------------------------------------------------
>
> exception Foo
> let f x =
> if x =1 then raise Foo else ()
>
> ;;
>
> for i = 0 to 10_000_000 do
> try
> f 1
> with Foo -> ()
> done
That is rather short for a test. lets add 2 zeroes to the loop. And lets
call f 0 and f 1 to test both cases:
f 0: 17.72s user 0.02s system 99% cpu 17.792 total
f 1: 35.30s user 0.02s system 99% cpu 35.371 total
f 0: 11.60s user 0.02s system 99% cpu 11.655 total
> ------------------------------------------------------------------------
> test2.ml:
> ------------------------------------------------------------------------
> let f x =
> if x=1 then None else Some ()
>
> ;;
> for i = 0 to 10_000_000 do
> match f 1 with
> None -> ()
> | Some s -> s
> done
> ------------------------------------------------------------------------
f 1: 10.91s user 0.01s system 99% cpu 10.933 total
And lets test the speed when the exception is actualy exceptional:
let () =
exception Foo
let f x = if x =1 then raise Foo else ()
try
for i = 0 to 1000000000 do
f 0
done
with Foo -> ()
9.94s user 0.00s system 99% cpu 9.946 total
Someone said in deep recursions exceptions are faster because they don't
have to unwind the stack:
exception Result of int
let rec fac acc = function
| 1 -> raise (Result acc)
| n -> fac (n * acc) (n - 1)
let () =
for i = 0 to 100_000_000 do
try
fac 1 50
with Result _ -> ()
done
71.88s user 0.00s system 99% cpu 1:11.90 total
let rec fac acc = function
| 1 -> acc
| n -> fac (n * acc) (n - 1)
let () =
for i = 0 to 100_000_000 do
ignore (fac 1 50)
done
67.04s user 0.02s system 99% cpu 1:07.08 total
Not feeling it. Lets try something not tail recursive:
exception Error
let rec foo = function
| 1 -> raise Error
| n -> n * (foo (n - 1))
let () =
for i = 0 to 100_000_000 do
try
ignore (foo 50)
with Error -> ()
done
25.03s user 0.01s system 99% cpu 25.068 total
let rec foo = function
| 1 -> None
| n -> match foo (n - 1) with None -> None | Some x -> Some (n * x)
let () =
for i = 0 to 100_000_000 do
ignore (foo 50)
done
49.48s user 0.01s system 99% cpu 49.508 total
In conclusion I would have to say that exceptions are better if they are
exceptional.
When you do not catch them right away or not at all then they are
better. The "try" command is more expensive than an option type and if
you are going to catch the exception right away anyway then options are
faster.
But if you don't catch them right away the cost of the try can be
amortized over many calls and becomes cheaper. Or if you don't catch the
exception at all then you can get a nice backtrace of where the
exception occured.
If your code is not tail recursive then option types mean you have to
match them on every level again and again and allocate a ton of 'Some x'
if no error occurs. You can make your code tail recursive or use
exception to improve performance there.
If you are writing a module then consider providing both flavours for
functions, one with exceptions and one with options. Even if you only do
something like this:
let find x y = ....
let raise_on_none exn f arg =
match f arg with
| None -> raise exn
| Some x -> x
let find_exn x y = raise_on_none Not_found (find x) y
Obviously option only works for exceptions like Not_found. If you want
to return an error you might have to use something like
type ('a, 'b) result = Result of 'a | Error of 'b
Putting the 2 flavours into different submodules can keep things tidy too.
MfG
Goswin