28/05: quicksort implementation comparisons
i set out to compare the relative speeds of some slightly different versions of quicksort for lists.
i think the results are worth noting.
let rec qsort_else f lst =
if lst = [] then [] else
let pivot = List.hd lst
let left = List.tl lst |> List.filter (f pivot) |> qsort_else f
let right = List.tl lst |> List.filter (f pivot >> not) |> qsort_else f
left @ [pivot] @ right
let rec qsort_append f = function
| [] -> []
| head :: tail ->
(tail |> List.filter (f head) |> qsort_append f)
@ [head] @
(tail |> List.filter (f head >> not) |> qsort_append f)
let rec qsort_pipe f = function
| [] -> []
| head :: tail ->
tail |> List.partition (f head)
|> (fun (x, y) -> qsort_pipe f x, qsort_pipe f y)
|> (fun (left, right) -> left @ [head] @ right)
let qsort_CPS f lst =
let rec sort l clst r c =
match clst with
| [] -> c (l @ [] @ r)
| a :: [] -> c (l @ [a] @ r)
| _ ->
let pivot = List.hd clst
let left = List.tl clst |> List.filter (f pivot)
let right = List.tl clst |> List.filter (f pivot >> not)
sort l left [pivot] (fun x -> sort x right r c)
sort [] lst [] (fun x -> x)
i also compared these four versions with the default List.sort and the mergesort which i copied out of local.fs from the f# source (search for stable_sort).
i only tested one kind of input data: a large, unsorted list comprised of random integer data in the same range as the length of the list. specifically generated this way:
let _random = System.Random()
let rand() = _random.Next()
let num = 200000
let unsorted = [1..num] |> List.map (fun _ -> rand() % num)
i ran each sorting method on the same unsorted list five times and averaged the result in milliseconds.
these are the results using version 1.9.6.2 of the f# compiler:
List.sort: 500, 457, 525, 468, 468, --> average: 483.600000
merge sort: 510, 482, 463, 542, 467, --> average: 492.800000
qsort_else: 942, 917, 944, 968, 953, --> average: 944.800000
qsort_append: 888, 836, 854, 890, 807, --> average: 855.000000
qsort_pipe: 672, 727, 751, 788, 669, --> average: 721.400000
qsort_CPS:
+the continuation passing style version of quicksort overflows the stack before it returns any meaningful data. if given a much smaller dataset it is still at least 100 times slower than the other sorting methods.
+pattern matching on a list appears to be slightly faster than an if/then/else case that does the same thing.
+the fastest quicksort method pipes the list through List.partition instead of filtering twice.
+an optimized mergesort is faster than any of my quicksort implementations and this is the method that List.sort uses internally i believe.
the most interesting information though, came when i ran the same tests on the most recently released version of f#
these are the results compiled using f# 1.9.6.16:
List.sort: 1507, 1405, 1408, 1450, 1339, --> average: 1421.800000
merge sort: 834, 768, 741, 723, 706, --> average: 754.400000
qsort_else: 1125, 1104, 1051, 1099, 1013, --> average: 1078.400000
qsort_append: 1033, 946, 1000, 1073, 978, --> average: 1006.000000
qsort_pipe: 836, 860, 833, 748, 795, --> average: 814.400000
qsort_CPS:
+it is worth noting that ALL times are slightly slower but what is quite striking is:
+the native List.sort method is now almost twice as slow as my fastest quicksort method and three times slower than in the previous version of the compiler.
+since mergesort is still the fastest method (although about 60% slower than the 1.9.6.2 compiled version) and List.sort is operating so sluggishly it would lead me to believe that List.sort in the newest f# compiler no longer uses the mergesort method in local.fs.
what method does it use?
i think the results are worth noting.
let rec qsort_else f lst =
if lst = [] then [] else
let pivot = List.hd lst
let left = List.tl lst |> List.filter (f pivot) |> qsort_else f
let right = List.tl lst |> List.filter (f pivot >> not) |> qsort_else f
left @ [pivot] @ right
let rec qsort_append f = function
| [] -> []
| head :: tail ->
(tail |> List.filter (f head) |> qsort_append f)
@ [head] @
(tail |> List.filter (f head >> not) |> qsort_append f)
let rec qsort_pipe f = function
| [] -> []
| head :: tail ->
tail |> List.partition (f head)
|> (fun (x, y) -> qsort_pipe f x, qsort_pipe f y)
|> (fun (left, right) -> left @ [head] @ right)
let qsort_CPS f lst =
let rec sort l clst r c =
match clst with
| [] -> c (l @ [] @ r)
| a :: [] -> c (l @ [a] @ r)
| _ ->
let pivot = List.hd clst
let left = List.tl clst |> List.filter (f pivot)
let right = List.tl clst |> List.filter (f pivot >> not)
sort l left [pivot] (fun x -> sort x right r c)
sort [] lst [] (fun x -> x)
i also compared these four versions with the default List.sort and the mergesort which i copied out of local.fs from the f# source (search for stable_sort).
i only tested one kind of input data: a large, unsorted list comprised of random integer data in the same range as the length of the list. specifically generated this way:
let _random = System.Random()
let rand() = _random.Next()
let num = 200000
let unsorted = [1..num] |> List.map (fun _ -> rand() % num)
i ran each sorting method on the same unsorted list five times and averaged the result in milliseconds.
these are the results using version 1.9.6.2 of the f# compiler:
List.sort: 500, 457, 525, 468, 468, --> average: 483.600000
merge sort: 510, 482, 463, 542, 467, --> average: 492.800000
qsort_else: 942, 917, 944, 968, 953, --> average: 944.800000
qsort_append: 888, 836, 854, 890, 807, --> average: 855.000000
qsort_pipe: 672, 727, 751, 788, 669, --> average: 721.400000
qsort_CPS:
conclusions:
+the continuation passing style version of quicksort overflows the stack before it returns any meaningful data. if given a much smaller dataset it is still at least 100 times slower than the other sorting methods.
+pattern matching on a list appears to be slightly faster than an if/then/else case that does the same thing.
+the fastest quicksort method pipes the list through List.partition instead of filtering twice.
+an optimized mergesort is faster than any of my quicksort implementations and this is the method that List.sort uses internally i believe.
the most interesting information though, came when i ran the same tests on the most recently released version of f#
these are the results compiled using f# 1.9.6.16:
List.sort: 1507, 1405, 1408, 1450, 1339, --> average: 1421.800000
merge sort: 834, 768, 741, 723, 706, --> average: 754.400000
qsort_else: 1125, 1104, 1051, 1099, 1013, --> average: 1078.400000
qsort_append: 1033, 946, 1000, 1073, 978, --> average: 1006.000000
qsort_pipe: 836, 860, 833, 748, 795, --> average: 814.400000
qsort_CPS:
+it is worth noting that ALL times are slightly slower but what is quite striking is:
+the native List.sort method is now almost twice as slow as my fastest quicksort method and three times slower than in the previous version of the compiler.
+since mergesort is still the fastest method (although about 60% slower than the 1.9.6.2 compiled version) and List.sort is operating so sluggishly it would lead me to believe that List.sort in the newest f# compiler no longer uses the mergesort method in local.fs.
what method does it use?