Skip to content

Instantly share code, notes, and snippets.

@blacktaxi
Last active August 29, 2015 14:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save blacktaxi/47aaec55b00fcff91827 to your computer and use it in GitHub Desktop.
Save blacktaxi/47aaec55b00fcff91827 to your computer and use it in GitHub Desktop.
F#: DU vs Struct performance
> System.GC.Collect();;
Real: 00:00:00.121, CPU: 00:00:00.109, GC gen0: 1, gen1: 1, gen2: 1
val it : unit = ()
> createTestNoArray 100000000 StructDU.gen StructDU.fold () ();;
Real: 00:00:04.036, CPU: 00:00:04.046, GC gen0: 534, gen1: 0, gen2: 0
val it : unit = ()
> System.GC.Collect();;
Real: 00:00:00.035, CPU: 00:00:00.031, GC gen0: 1, gen1: 1, gen2: 1
val it : unit = ()
> createTestNoArray 100000000 RegularDU.gen RegularDU.fold () ();;
Real: 00:00:04.208, CPU: 00:00:04.218, GC gen0: 534, gen1: 1, gen2: 0
val it : unit = ()
>
Testing regular DU... timing overhead... timing benchmark... done!
-> 0.0005123956951 / 1951.616709
Testing struct 'DU'... timing overhead... timing benchmark... done!
-> 0.0008053143967 / 1241.751053
Testing regular DU, no array... timing overhead... timing benchmark... done!
-> 0.0008429675596 / 1186.285271
Testing struct 'DU', no array... timing overhead... timing benchmark... done!
-> 0.0008053096749 / 1241.758334
module Test =
open System
open System.Diagnostics
let bench (actionFactory : unit -> (unit -> unit),
minIterations : int option) =
let minIterations = defaultArg minIterations 1
let properTime (action : unit -> unit)
(minTime : TimeSpan option)
(minIterations : int) =
let time f =
let sw = new System.Diagnostics.Stopwatch()
sw.Start()
f ()
sw.Stop()
sw.Elapsed
let minTime = defaultArg minTime (TimeSpan.FromSeconds(1.0))
let totalTime = ref <| TimeSpan.FromSeconds(0.0)
let mutable totalIterations = 0
System.GC.Collect()
while !totalTime <= minTime do
totalTime := (!totalTime).Add(time <| fun () ->
for _ in 0 .. minIterations do
action ())
totalIterations <- totalIterations + minIterations
(!totalTime, totalIterations)
printf "timing overhead... "
let (overheadT, overheadI) = properTime (fun () -> ()) None 10000000
let overheadP = overheadT.TotalSeconds / (float overheadI)
printf "timing benchmark... "
let (benchT, benchI) = properTime (actionFactory ()) None minIterations
let benchP = benchT.TotalSeconds / (float benchI)
printf "done! "
benchP - overheadP
let createTest iters gen fold =
let af () = fun () ->
let d = Array.init iters gen
Array.fold fold 0 d |> ignore
af
let createTestNoArray iters gen fold =
let af () = fun () ->
let mutable state = 0
for i = 0 to iters do
state <- gen i |> fold state
()
af
let runTests cases =
for (name, af) in cases do
printf "Testing %s... " name
let p = bench (af, None)
printfn "\n-> %A / %A" p (1.0 / p)
module RegularDU =
type Result<'a, 'e> =
| Ok of 'a
| Error of 'e
let gen idx =
if idx % 2 = 0 then Ok idx
else Error false
let fold a b =
match b with
| Ok x -> a + x
| Error _ -> a
module StructDU =
[<Struct>]
type Result<'a, 'e> =
val Ok : 'a
val Error : 'e
val IsOk : bool
new(ok, err, isOk) = { Ok = ok; Error = err; IsOk = isOk }
let inline Ok x = Result(x, Unchecked.defaultof<_>, true)
let inline Error e = Result(Unchecked.defaultof<_>, e, false)
let inline (|Ok|Error|) (r : Result<_, _>) =
if r.IsOk then Ok r.Ok
else Error r.Error
let gen idx =
if idx % 2 = 0 then Ok idx
else Error false
let fold a b =
match b with
| Ok x -> a + x
| Error _ -> a
let tests = [
"regular DU", createTest 10000 RegularDU.gen RegularDU.fold;
"struct 'DU'", createTest 10000 StructDU.gen StructDU.fold;
"regular DU, no array", createTestNoArray 10000 RegularDU.gen RegularDU.fold;
"struct 'DU', no array", createTestNoArray 10000 StructDU.gen StructDU.fold;
]
runTests tests
@Rickasaurus
Copy link

Strange, I'm seeing much different results:

module Test 

open System
open System.Diagnostics

let bench (actionFactory : unit -> (unit -> unit),
           minIterations : int option) =
    let minIterations = defaultArg minIterations 1


    let properTime (action : unit -> unit)
                   (minTime : TimeSpan option)
                   (minIterations : int) =
        let time f =
            let sw = new System.Diagnostics.Stopwatch()
            sw.Start()
            f ()
            sw.Stop()
            sw.Elapsed

        let minTime = defaultArg minTime (TimeSpan.FromSeconds(1.0))
        let totalTime = ref <| TimeSpan.FromSeconds(0.0)
        let mutable totalIterations = 0

        System.GC.Collect()

        while !totalTime <= minTime do
            totalTime := (!totalTime).Add(time <| fun () ->
                for _ in 0 .. minIterations do
                    action ())

            totalIterations <- totalIterations + minIterations

        (!totalTime, totalIterations)

    printf "timing overhead... "
    let (overheadT, overheadI) = properTime (fun () -> ()) None 10000000
    let overheadP = overheadT.TotalSeconds / (float overheadI)

    printf "timing benchmark... "
    let (benchT, benchI) = properTime (actionFactory ()) None minIterations
    let benchP = benchT.TotalSeconds / (float benchI)

    printf "done! "
    benchP - overheadP

let createTest iters gen fold =
    let af () = fun () ->
        let d = Array.init iters gen
        Array.fold fold 0 d |> ignore
    af

let createTestNoArray iters gen fold =
    let af () = fun () ->
        let mutable state = 0
        for i = 0 to iters do
            state <- gen i |> fold state
        ()
    af

let runTests cases =
    for (name, af) in cases do
        printf "Testing %s... " name
        let p = bench (af, None)
        printfn "\n-> %A / %A" p (1.0 / p)

module RegularDU =
    type Result<'a, 'e> =
        | Ok of 'a
        | Error of 'e

    let gen idx =
        if idx % 2 = 0 then Ok idx
        else Error false

    let fold a b =
        match b with
        | Ok x -> a + x
        | Error _ -> a

module StructDU =
    [<Struct>]
    type Result<'a, 'e> =
        val Ok : 'a
        val Error : 'e
        val IsOk : bool
        new(ok, err, isOk) = { Ok = ok; Error = err; IsOk = isOk }

    let inline Ok x = Result(x, Unchecked.defaultof<_>, true)
    let inline Error e = Result(Unchecked.defaultof<_>, e, false)

    let inline (|Ok|Error|) (r : Result<_, _>) =
        if r.IsOk then Ok r.Ok
        else Error r.Error

    let gen idx =
        if idx % 2 = 0 then Ok idx
        else Error false

    let fold a b =
        match b with
        | Ok x -> a + x
        | Error _ -> a

module StructDUWithIf = 
    [<Struct>]
    type Result<'a, 'e> =
        val Ok : 'a
        val Error : 'e
        val IsOk : bool
        new(ok, err, isOk) = { Ok = ok; Error = err; IsOk = isOk }

    let inline Ok x = Result(x, Unchecked.defaultof<_>, true)
    let inline Error e = Result(Unchecked.defaultof<_>, e, false)

    let inline gen idx =
        if idx % 2 = 0 then Ok idx
        else Error false

    let fold a (b: Result<_,_>) =
        match b with
        | b when b.IsOk -> a + b.Ok
        | b -> a


let tests = [
    "regular DU", createTest 100000 RegularDU.gen RegularDU.fold;
    "struct 'DU'", createTest 100000 StructDU.gen StructDU.fold;
    "regular DU, no array", createTestNoArray 100000 RegularDU.gen RegularDU.fold;
    "struct 'DU', no array", createTestNoArray 100000 StructDU.gen StructDU.fold;
    "struct DU, no patternmatch", createTestNoArray 100000 StructDUWithIf.gen StructDUWithIf.fold;
]

(*
> Testing regular DU... timing overhead... timing benchmark... done! 
> 0.01775800912 / 56.31261889
Testing struct 'DU'... timing overhead... timing benchmark... done! 
-> 0.009387691107 / 106.5224653
Testing regular DU, no array... timing overhead... timing benchmark... done! 
-> 0.0040408514 / 247.4725995
Testing struct 'DU', no array... timing overhead... timing benchmark... done! 
-> 0.07614994144 / 13.13198646
Testing struct DU, no patternmatch... timing overhead... timing benchmark... done! 
-> 0.07455309859 / 13.41325872
*)

@blacktaxi
Copy link
Author

If I increase the number of iterations to 100000, I get this:

Testing regular DU... timing overhead... timing benchmark... done!
-> 0.02042700855 / 48.95479422
Testing struct 'DU'... timing overhead... timing benchmark... done!
-> 0.008006895147 / 124.8923561
Testing regular DU, no array... timing overhead... timing benchmark... done!
-> 0.008723692274 / 114.6303616
Testing struct 'DU', no array... timing overhead... timing benchmark... done!
-> 0.008351817527 / 119.7344167
Testing just struct, no array... timing overhead... timing benchmark... done!
-> 0.006903679734 / 144.8502883
Testing just non-generic struct, no array... timing overhead... timing benchmark... done!
-> 0.006558729743 / 152.4685479

Strange indeed. But I'm pretty sure the benchmark is flawed.

Last two tests I've added: 'just struct' is the same as yours 'no patternmatch', and 'non-generic' is the same but the Result type is also non-generic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment