Search code examples
f#discriminated-union

Slow conversion of F# discriminated union case to string


I have around 100k discriminated union cases I have to convert to strings, but it seems to be extremely slow.

As a comparison, the following executes (in F# interactive) in 3seconds on average :

open System

let buf = Text.StringBuilder()
let s = DateTime.Now

for i in 1 .. 100000 do
  Printf.bprintf buf "%A" "OtherFinancingInterest" //string
  buf.Length <- 0

printfn "elapsed : %0.2f" (DateTime.Now - s).TotalMilliseconds

While the following executes (also in F# interactive) in over a minute...

open System

let buf = Text.StringBuilder()
let s = DateTime.Now

for i in 1 .. 100000 do
  Printf.bprintf buf "%A" OtherFinancingInterest //DU
  buf.Length <- 0

printfn "elapsed : %0.2f" (DateTime.Now - s).TotalMilliseconds

The discriminated union has 25 values (the result is still extremely slow, around 16 seconds with two cases, but less so than with 25). Any idea if that is "normal" or if I may be doing something wrong ?

Many thanks


Solution

  • The %A format specifier pretty prints any F# value. It uses reflection to do so. It should only really be used for debugging purposes, and not in normal application code.

    Note that using %s in your first example using a string makes it a lot faster because there is no type checking needed at runtime.

    For the DU, there is a hack you could use to make the reflection only happen once on application load:

    type FinancingInterest =
        | OtherFinancingInterest
    
    open FSharp.Reflection
    let private OtherFinancingInterestStringMap =
        FSharpType.GetUnionCases typeof<FinancingInterest>
        |> Array.map (fun c -> FSharpValue.MakeUnion(c, [||]) :?> FinancingInterest)
        |> Array.map (fun x -> x, sprintf "%A" x)
        |> Map.ofArray
    
    type FinancingInterest with
        member this.AsString = OtherFinancingInterestStringMap |> Map.find this
    

    You would also use this with the %s format specifier:

    Printf.bprintf buf "%s" OtherFinancingInterest.AsString
    

    I had similar timings to yours in your example, and now this one comes down to 40ms.

    This only works as long as all of the DU cases don't have an arguments. You will get an exception on application load as soon as you try anything like this:

    type FinancingInterest =
        | Foo of string
        | OtherFinancingInterest
    

    Having said all this, I think you're better off writing a simple function that explicitly converts your type into a string value, writing out the names in full with repetition if necessary. The names of discriminated union cases should not generally be thought of as data that affects your program. You would usually expect to be able to safely rename case names without affecting runtime behaviour at all.