Skip to content

Commit

Permalink
first complete benchmark for #18
Browse files Browse the repository at this point in the history
  • Loading branch information
dadhi committed Apr 23, 2019
1 parent af0a418 commit e660caa
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 167 deletions.
2 changes: 2 additions & 0 deletions ImTools.sln.DotSettings
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=Haskell/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
74 changes: 32 additions & 42 deletions playground/ImTools.UnionCsVsFsBenchmarks/Program.fs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
(*
| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op |
|------------------------------------ |-----------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:|
| FSharp | 5.920 ns | 0.0323 ns | 0.0286 ns | 1.00 | 0.00 | - | - | - | - |
| CSharp_named_case | 138.089 ns | 0.4042 ns | 0.3583 ns | 23.33 | 0.16 | - | - | - | - |
| CSharp_unnamed_case | 142.634 ns | 0.7154 ns | 0.6692 ns | 24.09 | 0.18 | - | - | - | - |
| CSharp_unnamed_case_as_CaseN_struct | 9.267 ns | 0.0291 ns | 0.0258 ns | 1.57 | 0.01 | - | - | - | - |
| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op |
|------------------------------------------- |----------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:|
| FSharp | 11.44 ns | 0.0769 ns | 0.0719 ns | 1.00 | 0.00 | - | - | - | - |
| CSharp_named_case_struct | 18.32 ns | 0.0505 ns | 0.0473 ns | 1.60 | 0.01 | - | - | - | - |
| CSharp_named_case_struct_match_I_interface | 304.78 ns | 5.8215 ns | 5.4454 ns | 26.64 | 0.48 | - | - | - | - |
| CSharp_named_case_struct_Match_method | 134.24 ns | 0.4707 ns | 0.4403 ns | 11.74 | 0.09 | 0.1729 | - | - | 816 B |
*)

open BenchmarkDotNet.Attributes
Expand All @@ -14,25 +14,24 @@ open ImTools
type FlagOrCount =
| Flag of bool
| Count of int
| Message of string

type Flag1() = class inherit I<Flag1, bool>() end
type Count1() = class inherit I<Count1, int>() end
type FlagOrCount1 = class inherit Union<FlagOrCount1, I<Flag1, bool>.v, I<Count1, int>.v> end

type Flag2() = class inherit Is<Flag2, bool>() end
type Count2() = class inherit Is<Count2, int>() end
type FlagOrCount2 = class inherit Union<FlagOrCount2, Flag2, Count2> end

type FlagOrCount3 = class inherit Union<FlagOrCount3, bool, int> end
type Flag1() = class inherit Val<Flag1, bool>() end
type Count1() = class inherit Val<Count1, int>() end
type Message1() = class inherit Val<Message1, string>() end

// NOTE: It happens that in F# is not possible to refer to nested type from the inheritor - commented below.
// It is much less verbose in C#. The same problem is with `match` clause later in the benchmarks.
//
// type FlagOrCount1 = class inherit U<FlagOrCount1, Flag1.value, Count1.value, Message1.value> end
//
type FlagOrCount1 = class inherit U<FlagOrCount1, Val<Flag1, bool>.value, Val<Count1, int>.value, Val<Message1, string>.value> end

[<MemoryDiagnoser>]
type Traverse_union_array_and_match_to_sum_something() =

let _fs = [| Flag true; Count 1 |]
let _cs1 = [| FlagOrCount1.Of (Flag1.Of true); FlagOrCount1.Of (Count1.Of 1) |]
let _cs2 = [| FlagOrCount2.Of (Flag2.Of true); FlagOrCount2.Of (Count2.Of 1) |]
let _cs3 = [| FlagOrCount3.Of true; FlagOrCount3.Of 1 |]
let _fs = [| Flag true; Count 1; Message "hey" |]
let _cs1 = [| FlagOrCount1.Of (Flag1.Of true); FlagOrCount1.Of (Count1.Of 1); FlagOrCount1.Of (Message1.Of "hey") |]

[<Benchmark(Baseline = true)>]
member this.FSharp() =
Expand All @@ -41,48 +40,39 @@ type Traverse_union_array_and_match_to_sum_something() =
match x with
| Flag f -> if f then sum <- sum + 1 else sum |> ignore
| Count n -> sum <- sum + n
| Message _ -> sum <- sum + 42
sum

[<Benchmark>]
member this.CSharp_named_case_struct() =
let mutable sum = 0
for x in _cs1 do
match x with
| :? Union<FlagOrCount1, I<Flag1, bool>.v, I<Count1, int>.v>.Case1 as f -> if f.X.X then sum <- sum + 1 else sum |> ignore
| :? Union<FlagOrCount1, I<Flag1, bool>.v, I<Count1, int>.v>.Case2 as n -> sum <- sum + n.X.X
| _ -> ()
sum

//[<Benchmark>]
member this.CSharp_named_case() =
let mutable sum = 0
for x in _cs2 do
match x with
| :? Is<Flag2> as f -> if f.Value.Value then sum <- sum + 1 else sum |> ignore
| :? Is<Count2> as n -> sum <- sum + n.Value.Value
| :? U<FlagOrCount1, Val<Flag1, bool>.value, Val<Count1, int>.value, Val<Message1, string>.value>.case1 as f -> if f.X.X then sum <- sum + 1 else sum |> ignore
| :? U<FlagOrCount1, Val<Flag1, bool>.value, Val<Count1, int>.value, Val<Message1, string>.value>.case2 as n -> sum <- sum + n.X.X
| :? U<FlagOrCount1, Val<Flag1, bool>.value, Val<Count1, int>.value, Val<Message1, string>.value>.case3 -> sum <- sum + 42
| _ -> ()
sum

//[<Benchmark>]
member this.CSharp_unnamed_case() =
[<Benchmark>]
member this.CSharp_named_case_struct_match_I_interface() =
let mutable sum = 0
for x in _cs3 do
for x in _cs1 do
match x with
| :? Is<bool> as f -> if f.Value then sum <- sum + 1 else sum |> ignore
| :? Is<int> as n -> sum <- sum + n.Value
| :? I<Val<Flag1, bool>.value> as f -> if f.Value.X then sum <- sum + 1 else sum |> ignore
| :? I<Val<Count1, int>.value> as n -> sum <- sum + n.Value.X
| :? I<Val<Message1, string>.value> -> sum <- sum + 42
| _ -> ()
sum

[<Benchmark>]
member this.CSharp_unnamed_case_as_CaseN_struct() =
member this.CSharp_named_case_struct_Match_method() =
let mutable sum = 0
for x in _cs3 do
match x with
| :? Union<FlagOrCount3, bool, int>.Case1 as f -> if f.X then sum <- sum + 1 else sum |> ignore
| :? Union<FlagOrCount3, bool, int>.Case2 as n -> sum <- sum + n.X
| _ -> ()
for x in _cs1 do
x.Match((fun f -> if f.X then sum <- sum + 1 else sum |> ignore), (fun n -> sum <- sum + n.X), (fun m -> sum <- sum + 42)) |> ignore
sum


[<EntryPoint>]
let main argv =
BenchmarkRunner.Run<Traverse_union_array_and_match_to_sum_something>() |> ignore
Expand Down
48 changes: 24 additions & 24 deletions playground/ImTools.UnionPlayground/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ class Program
static void Main()
{
// Unnamed (anonymous) union is fast to declare and use
var i = U<int, string>.Of(42);
var s = U<int, string>.Of("hey");
var i = U2<int, string>.Of(42);
var s = U2<int, string>.Of("hey");

WriteLine(i);
WriteLine(s);

// You may create the union case directly via constructor, helpful for cases like `U<A, A>` or `U<string, string>`
var s2 = new U<int, string>.Case1(24);
var s2 = new U2<int, string>.case1(24);
WriteLine(s2);

// Typed union, the type is different from U<int, string>, e.g. `s = name;` won't compile
Expand Down Expand Up @@ -48,15 +48,15 @@ static void Main()
}

// One line named union definition
public sealed class FlagOrName : Union<FlagOrName, bool, string> { }
public sealed class FlagOrName : U<FlagOrName, bool, string> { }

// A different type from FlagOrName
public sealed class Other : Union<Other, bool, string> { }
public sealed class Other : U<Other, bool, string> { }

// Typed union with a typed cases! Now you can pattern match via `I<Flag>` and `I<Name>`
public sealed class FlagOrName2 : Union<FlagOrName2, Flag, Name> { }
public sealed class Flag : Is<Flag, bool> { }
public sealed class Name : Is<Name, string> { }
public sealed class FlagOrName2 : U<FlagOrName2, Flag.value, Name.value> { }
public sealed class Flag : Val<Flag, bool> { }
public sealed class Name : Val<Name, string> { }

public static class Usage
{
Expand All @@ -65,8 +65,8 @@ public static string PatternMatching<T>(T x) where T : FlagOrName.union
{
switch (x)
{
case FlagOrName.Case1 b: return "" + b; // b.Value for the actual value
case FlagOrName.Case2 s: return "" + s;
case FlagOrName.case1 b: return "" + b; // b.Value for the actual value
case FlagOrName.case2 s: return "" + s;
default: throw new NotSupportedException();
}
}
Expand All @@ -78,33 +78,33 @@ public static string PatternMatchingWithTypedCases(FlagOrName2.union x)
// The performance price may be gained back any time by switching to CaseN struct matching.
switch (x)
{
case Is<Flag> b: return "" + b; // b.Value.Value for the actual value
case I<Flag> b: return "" + b; // b.Value.Value for the actual value
//case Is<Name> s: return "" + s;
default: throw new NotSupportedException();
}
}
}

public class Option<T> : Union<Option<T>, Empty, T>
public class Option<T> : U<Option<T>, Empty, T>
{
// Type specific custom Case names
public static readonly Case1 None = new Case1(Empty.Value);
public static Case2 Some(T x) => new Case2(x);
public static readonly case1 None = new case1(Empty.Value);
public static case2 Some(T x) => new case2(x);
}

// Named facades for the cases are the best for inference and simplicity of use.
public static class None
{
public static Option<T>.Case1 Of<T>() => Option<T>.None;
public static Option<T>.case1 Of<T>() => Option<T>.None;
}

public static class Some
{
public static Option<T>.Case2 Of<T>(T x) => Option<T>.Some(x);
public static Option<T>.case2 Of<T>(T x) => Option<T>.Some(x);
}

// Enum without additional state in a Union disguise. Can be pattern matched as `case Is<Increment> _: ...; break;`
public sealed class CounterMessage : Union<CounterMessage, Increment, Decrement> { }
public sealed class CounterMessage : U<CounterMessage, Increment, Decrement> { }
public struct Increment { }
public struct Decrement { }

Expand All @@ -121,10 +121,10 @@ public struct Decrement { }
//public sealed class ListY<T> : Union<ListY<T>, Unit, ListY<T>.I> { }

// Recursive definition of the List
public sealed class MyList<T> : Union<MyList<T>, Empty, MyList<T>.NonEmptyList>
public sealed class MyList<T> : U<MyList<T>, Empty, MyList<T>.NonEmptyList>
{
public static readonly Case1 Empty = new Case1(ImTools.Empty.Value);
public static Case2 NonEmpty(T head, union tail) => new Case2(new NonEmptyList(head, tail));
public static readonly case1 Empty = new case1(ImTools.Empty.Value);
public static case2 NonEmpty(T head, union tail) => new case2(new NonEmptyList(head, tail));

public readonly struct NonEmptyList
{
Expand All @@ -137,14 +137,14 @@ public readonly struct NonEmptyList

public static class MyList
{
public static MyList<T>.Case2 Push<T>(this MyList<T>.union list, T head) => MyList<T>.NonEmpty(head, list);
public static MyList<T>.case2 Push<T>(this MyList<T>.union list, T head) => MyList<T>.NonEmpty(head, list);
}

// Less efficient, but less boilerplate recursive type - requires one heap reference per recursive type usage.
public sealed class MyTree<T> : Union<MyTree<T>, Empty, MyTree<T>.NonEmptyTree>
public sealed class MyTree<T> : U<MyTree<T>, Empty, MyTree<T>.NonEmptyTree>
{
public sealed class NonEmptyTree : Is<NonEmptyTree, (union Left, T Leaf, union Right)> { }
public static readonly Case1 Empty = new Case1(ImTools.Empty.Value);
public sealed class NonEmptyTree : Rec<NonEmptyTree, (union Left, T Leaf, union Right)> { }
public static readonly case1 Empty = new case1(ImTools.Empty.Value);
}

public static class MyTree
Expand Down
Loading

0 comments on commit e660caa

Please sign in to comment.