I became interested functor module of ocaml and wrote small program within:
module type ORDERED = sig
type t
val compare : t -> t -> int
end
module MakeSet (Elt : ORDERED) = struct
type elt = Elt.t
type t = elt list (* упорядоченный список без дублей *)
let empty = []
let rec add x = function
| [] -> [x]
| y :: rest as s ->
let c = Elt.compare x y in
if c = 0 then s (* уже есть *)
else if c < 0 then x :: s (* вставка перед y *)
else y :: add x rest
let mem x s = List.exists (fun y -> Elt.compare x y = 0) s
end
module IntOrd = struct
type t = int
let compare = compare (* стандартное сравнение *)
end
module IntSet = MakeSet (IntOrd)
let rec loop n xs =
if n < 1 then xs else loop (n - 1) (IntSet.add n xs)
let n = 10_000_000
let x = loop n IntSet.empty
let b = IntSet.mem n x
let () = Printf.printf "%b\ndone\n" b
let empty = []
let inline compare<'t when 't :> System.IComparable> (a: 't, b: 't) = a.CompareTo(b)
let rec add x =
function
| [] -> [ x ]
| y :: rest as s ->
let c = compare (x, y) in
if c = 0 then
s (* уже есть *)
else if c < 0 then
x :: s (* вставка перед y *)
else
y :: add x rest
let mem x s =
List.exists (fun y -> compare (x, y) = 0) s
let rec loop n xs =
if n < 1 then
xs
else
loop (n - 1) (add n xs)
let n = 10_000_000
let x = loop n empty
let b = mem n x
do printf "%b\ndone\n" b
Measure-Command { & .\Test.exe } - 4-6 sec (target mscorlib)
Measure-Command { & .\camlprog.exe } - 2 sec
Why? Can I something change to reach ocaml performance?
I’m guessing the main contributor to the performance difference is the F# version currently boxing arguments passed to CompareTo():
let inline compare<'t when 't :> System.IComparable> (a: 't, b: 't) = a.CompareTo(b)
IComparable.CompareTo() takes an object. The argument passed (in this case an int) needs to be boxed before the comparison is done.
Looking at the IL confirms this:
ldarg.1
box !!T
constrained. !!T
callvirt instance int32 System.IComparable::CompareTo(object)
If you change the compare line to…
let inline compare<'t when 't :> IComparable<'t>> (a: 't, b: 't) = a.CompareTo(b)
… then you can avoid the boxing penalty in this case.
IL confirmation (note the absent box !!T):
ldarga.s a
ldarg.1
constrained. !!T
callvirt instance int32 System.IComparable`1<!!T>::CompareTo(!0)
I ran a quick benchmark using .NET 10, and here’s what I got comparing the boxed vs non-boxed versions:
| Method |
Mean |
Error |
StdDev |
Ratio |
RatioSD |
Gen0 |
Gen1 |
Gen2 |
Allocated |
Alloc Ratio |
| Original |
2,057.2 ms |
62.33 ms |
32.60 ms |
1.00 |
0.02 |
150000.0000 |
49000.0000 |
5000.0000 |
762.9 MB |
1.00 |
| GenericComparable |
757.2 ms |
15.63 ms |
8.18 ms |
0.37 |
0.01 |
54000.0000 |
28000.0000 |
3000.0000 |
305.18 MB |
0.40 |
Full benchmarking code
open System
open BenchmarkDotNet.Attributes
open BenchmarkDotNet.Running
module Original =
let empty: int list = []
let inline compare<'T when 'T :> IComparable> (a: 'T, b: 'T) = a.CompareTo(b)
let rec add x =
function
| [] -> [ x ]
| y :: rest as s ->
let c = compare (x, y)
if c = 0 then s
elif c < 0 then x :: s
else y :: add x rest
let mem x s =
List.exists (fun y -> compare (x, y) = 0) s
let rec loop n xs =
if n < 1 then xs else loop (n - 1) (add n xs)
let run n =
let x = loop n empty
mem n x
module GenericComparable =
let empty: int list = []
// Generic IComparable<T>; CompareTo takes T rather than obj.
let inline compare<'T when 'T :> IComparable<'T>> (a: 'T, b: 'T) = a.CompareTo(b)
let rec add x =
function
| [] -> [ x ]
| y :: rest as s ->
let c = compare (x, y)
if c = 0 then s
elif c < 0 then x :: s
else y :: add x rest
let mem x s =
List.exists (fun y -> compare (x, y) = 0) s
let rec loop n xs =
if n < 1 then xs else loop (n - 1) (add n xs)
let run n =
let x = loop n empty
mem n x
[<MemoryDiagnoser>]
[<SimpleJob (launchCount = 1, warmupCount = 3, iterationCount = 10, invocationCount = 1)>]
type CompareBenchmarks () =
let n = 10_000_000
[<Benchmark(Baseline = true)>]
member _.Original() = Original.run n
[<Benchmark>]
member _.GenericComparable() = GenericComparable.run n
[<EntryPoint>]
let main _ =
BenchmarkRunner.Run<CompareBenchmarks>() |> ignore
0
OMG! Thank you very much. Very small detail but very important!