Reading Beginning F# - Robert Pickering I focused on the following paragraph:
Programmers coming from an OCaml background should be careful when using exceptions in F#. Because of the architecture of the CLR, throwing an exception is pretty expensive—quite a bit more expensive than in OCaml. If you throw a lot of exceptions, profile your code carefully to decide whether the performance costs are worth it. If the costs are too high, revise the code appropriately.
Why, because of CLR, throwing an exception is more expensive if F#
than in OCaml
? And what is the best way to revise the code appropriately in this case?
OCaml has exception handling to deal with exceptions, application-level errors, out-of-band data and the like. Exceptions are a very dynamic construct, but there is a third-party application that performs a static analysis of your use of exceptions and can find inconsistencies and errors at compile time!
The simplest one is failwith , which could be defined as follows: let failwith msg = raise (Failure msg);; val failwith : string -> 'a = <fun> OCaml. There are several other useful functions for raising exceptions, which can be found in the API documentation for the Common and Exn modules in Base.
One way of handling errors in OCaml is exceptions. The standard library relies heavily upon them. Exceptions belong to the type exn (an extensible sum type): exception Foo of string let i_will_fail () = raise (Foo "Oh no!")
Exceptions in the CLR are very rich, and provide a lot of detail. Rico Mariani posted an (old, but still relevant) post on the cost of exceptions in the CLR which details some of this.
As such, throwing an exception in the CLR has a higher relative cost than in some other environments, including OCaml.
And what is the best way to revise the code appropriately in this case?
If you expect the exception to be raised in normal, non-exceptional circumstances, you can rethink your algorithm and API in a way that avoids the exception entirely. Try to provide an alternative API where you can test for the circumstance prior to causing the exception to be raised, for example.
Reed already explained why .NET exceptions behave differently than OCaml exceptions. In general, .NET exceptions are suitable only for exceptional situations and are designed for that purpose. OCaml has more lightweight model and so they are used to also implement some control flow patterns.
To give a concrete example, in OCaml you could use exception to implement breaking of a loop. For example, say you have a function test
that tests whether a number is a number we want. The following iterates over numbers from 1 to 100 and returns first matching number:
// Simple exception used to return the result
exception Returned of int
try
// Iterate over numbers and throw if we find matching number
for n in 0 .. 100 do
printfn "Testing: %d" n
if test n then raise (Returned n)
-1 // Return -1 if not found
with Returned r -> r // Return the result here
To implement this without exceptions, you have two options. You could write a recursive function that has the same behaviour - if you call find 0
(and it is compiled to essentially the same IL code as using return n
inside for
loop in C#):
let rec find n =
printfn "Testing: %d" n
if n > 100 then -1 // Return -1 if not found
elif test n then n // Return the first found result
else find (n + 1) // Continue iterating
The encoding using recursive functions can be a bit lengthly, but you can also use standard functions provided by the F# library. This is often the best way to rewrite code that would use OCaml exceptions for control flow. In this case, you can write:
// Find the first value matching the 'test' predicate
let res = seq { 0 .. 100 } |> Seq.tryFind test
// This returns an option type which is 'None' if the value
// was not found and 'Some(x)' if the value was found.
// You can use pattern matching to return '-1' in the default case:
match res with
| None -> -1
| Some n -> n
If you're not familiar with option types, then take a look at some introductory material. F# wikibook has a good tutorial and MSDN documentation has useful examples too.
Using an appropriate function from the Seq
module often makes the code a lot shorter, so it is preferrable. It may be slightly less efficient than using recursion directly, but in most of the cases, you don't need to worry about that.
EDIT: I was curious about the actual performance. The version using Seq.tryFind
is more efficient if the input is lazily generated sequence seq { 1 .. 100 }
instead of a list [ 1 .. 100 ]
(because of the costs of list allocation). With these changes, and test
function that returns 25th element, the time needed to run the code 100000 times on my machine is:
exceptions 2.400sec
recursion 0.013sec
Seq.tryFind 0.240sec
This is extremely trivial sample, so I think the solution using Seq
will not generally run 10 times slower than equivalent code written using recursion. The slowdown is probably due to the allocation of additional data structures (object representing the sequence, closures, ...) and also due to additional indirection (the code needs numerous virtual method calls, instead of just plain numeric operations and jumps). However, exceptions are even more costly and don't make the code shorter or more readable in any way...
Why, because of CLR, throwing an exception is more expensive if F# than in OCaml?
OCaml was heavily optimized for the use of exceptions as control flow. In contrast, exceptions in .NET have not been optimized at all.
Note that the performance difference is huge. Exceptions are around 600× faster in OCaml than they are in F#. Even C++ is around 6× slower than OCaml in this regard, according to my benchmarks.
Even though .NET exceptions allegedly provide more (OCaml provides stack traces, what more do you want?) I cannot think of any reason why they should be as slow as they are.
And what is the best way to revise the code appropriately in this case?
In F#, you should write "total" functions. This means your function should return a value of a union type indicating the kind of result, e.g. normal or exceptional.
In particular, calls to find
should be replaced with calls to tryFind
that return a value of the option
type that is either Some
value or None
if the key element was not present in the collection.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With