1

I am benchmarking insertion sort on worst case input (reverse ordered list) and random input.

import Control.Monad
import Data.List
import System.Random
import Control.Exception
import Control.DeepSeq
import Criterion.Main

--- Sorting ---
insertionSort :: (Ord a) => [a] -> [a]
insertionSort [] = []
insertionSort (x:xs) = x `insert` (sort xs)

--- Generators ---
worstCaseGen :: Int -> [Int]
worstCaseGen n = [n, n-1..1]

bestCaseGen :: Int -> [Int]
bestCaseGen n = [1..n]

randomGen :: Int -> StdGen -> [Int]
randomGen n = take n . randoms

--- Testing ---
main = do
  gen <- newStdGen
  randomList <- evaluate $ force $ randomGen 10000 gen
  defaultMain [
    bgroup "Insertion Sort" [ bench "worst" $ nf insertionSort (worstCaseGen 10000)
                            , bench "best" $ nf insertionSort (bestCaseGen 10000)
                            , bench "gen" $ nf last randomList
                            , bench "random" $ nf insertionSort randomList
                            ]
    ]

While random input should perform at about the same magnitude as the worst case input, in reality the benchmark shows that it is about 20 times slower. My guess is that branch prediction kicks in and the random case is very hard to predict and thus becomes slower. Could this be true?

This is my .cabal if it helps:

executable BranchPrediction
  main-is:             Main.hs
  build-depends:       base >=4.12 && <4.13,
                       random,
                       criterion ==1.5.4.0,
                       deepseq ==1.4.4.0
  default-language:    Haskell2010
sqd
  • 1,485
  • 13
  • 23
  • How are you actually compiling and testing your code? – Marc Talbot Mar 30 '19 at 00:12
  • @MarcTalbot I have edited in my .cabal config if that would help. – sqd Mar 30 '19 at 00:14
  • So you are building as unoptimized code? – Marc Talbot Mar 30 '19 at 00:16
  • @MarcTalbot I think GHC does optimization by default? I did try `-O0` but it didn't change anything. Not even the speed of the sorting. – sqd Mar 30 '19 at 00:24
  • 2
    GHC does not do optimization by default. `-O0` means "don't do optimization", and is the default. `-O2` is the first flag to try for enabling optimizations. – Daniel Wagner Mar 30 '19 at 00:26
  • @DanielWagner Thank you for pointing that out! I just tried `-O2`but it doesn't change anything. – sqd Mar 30 '19 at 00:30
  • From what I remember of dabbling at the generated code level, lazy evaluation pretty much destroys any hope of predicting branching. The predictor doesn't understand what a thunk is. – AntC Mar 30 '19 at 00:43
  • You can use `perf` to look at branch misses (and a lot of other metrics), and build an intuition for these things. A better first place to start though is just running with `RTS -s` – jberryman Mar 30 '19 at 04:56

1 Answers1

7

You called sort instead of insertionSort in your (supposed-to-be-) recursive case. That's a run-optimized merge sort, which handles reversed input in O(n) time. So your "worst case" is actually a near-best case for the algorithm as written, rather than as intended.

Carl
  • 26,500
  • 4
  • 65
  • 86