packages feed

bytestring-trie-0.2.7.6: dev/Bench/MatchOne.hs

{-# OPTIONS_GHC -Wall -fwarn-tabs #-}
{-# LANGUAGE CPP, BangPatterns #-}

----------------------------------------------------------------
--                                                  ~ 2023.03.19
-- |
-- Module      :  Bench.MatchOne
-- Copyright   :  2008--2023 wren romano
-- License     :  BSD-3-Clause
-- Maintainer  :  wren@cpan.org
-- Stability   :  provisional
-- Portability :  portable
--
-- Benchmarking definitions for 'TI.match_'
----------------------------------------------------------------

module Main (main) where

import qualified Data.Trie           as T
import qualified Data.Trie.Internal  as TI
import qualified Data.ByteString     as S
#if !(MIN_VERSION_base(4,18,0))
-- [base-4.18 / GHC 9.6.1]: Prelude now re-exports 'liftA2'.
import           Control.Applicative (liftA2)
#endif
import           Control.DeepSeq     (NFData)
import qualified Test.QuickCheck     as QC
import qualified Criterion.Main      as C
-- TODO: consider trying <https://hackage.haskell.org/package/tasty-bench>.  Especially so we can add tests to ensure that all these implementations are correct; but also for the comparisons across versions (since @bench-show@ and @benchgraph@ are both too heavy-weight for our needs).
----------------------------------------------------------------

-- Using NOINLINE to improve the reliability\/quality of the benchmarks.
match_v027, match_foldl, match_foldr, match_foldr_alt
    :: T.Trie a -> S.ByteString -> Maybe (Int, a)

-- The manual implementation.
{-# NOINLINE match_v027 #-}
match_v027 = TI.match_

-- This implementation is based on @base-4.16.0.0@:'GHC.List.last'.
-- N.B., the implementation uses 'foldl' in order to be a good
-- consumer, contrary to the 'foldr' we would have expected; see:
-- <https://gitlab.haskell.org/ghc/ghc/-/issues/9339>
-- Also, the actual definition of 'GHC.List.last' must be eta-expanded
-- in order to actually have 'foldl' make it a good consumer; see:
-- <https://gitlab.haskell.org/ghc/ghc/-/issues/10260>
-- (Not that that's relevant for us, since we're inlining the
-- definition anyways, and the call-site is indeed saturated.)
--
-- TODO: since when does 'foldl' make good consumers?  Did GHC
-- switch from build\/foldr to unfold\/destroy?
--
-- Per the benchmarks below, this one is by far the slowest; thereby
-- suggesting that 'foldl' is not in fact a good consumer!
{-# NOINLINE match_foldl #-}
match_foldl t q = foldl (\_ x -> Just x) Nothing (TI.matches_ t q)

-- This uses the definition of 'GHC.List.last' prior to #9339.  Note
-- how @step@ returns the @Just@ immediately, and only lazily does
-- the case analysis.
--
-- Per the benchmarks below, this is better than 'match_foldl' but
-- not as good as 'match_foldr_alt'
{-# NOINLINE match_foldr #-}
match_foldr t q = foldr step Nothing (TI.matches_ t q)
    where
    step x m = Just (case m of { Nothing -> x; Just y -> y })

-- And here's a version that doesn't do that @Just@-before-case...
--
-- Per the benchmarks below, this is better than 'match_foldr' but
-- still not as good as 'match_v027'.
{-# NOINLINE match_foldr_alt #-}
match_foldr_alt t q = foldr step Nothing (TI.matches_ t q)
    where
    step x Nothing = Just x
    step _ y       = y

-- TODO: maybe try a Codensity version of the above two, to avoid
-- redundant case analysis on the intermediate 'Maybe'

----------------------------------------------------------------
-- TODO: move this stuff off to the shared file.
-- TODO: should have argument to bound the 'Word8'
arbitraryBS :: Int -> QC.Gen S.ByteString
arbitraryBS maxL = do
    l  <- QC.chooseInt (0, maxL)
    xs <- QC.vector l
    return $! S.pack xs

arbitraryTrie :: Int -> Int -> QC.Gen (T.Trie Int)
arbitraryTrie maxK maxL = do
    k    <- QC.chooseInt (0, maxK)
    keys <- QC.vectorOf k $ arbitraryBS maxL
    return $! T.fromList (zip keys [0..])

-- TODO: really need a better environment to work on than this...
generateEnv :: IO ([T.Trie Int], [S.ByteString])
generateEnv = QC.generate $ do
    ts <- QC.vectorOf 10 $ arbitraryTrie 30 10
    qs <- QC.vectorOf 10 $ arbitraryBS 12
    return (ts, qs)

cartesianNF :: NFData c => (a -> b -> c) -> [a] -> [b] -> C.Benchmarkable
cartesianNF f xs ys = C.nf (liftA2 f xs) ys

----------------------------------------------------------------
-- TODO: <https://github.com/haskell/containers/blob/master/containers-tests/benchmarks/IntMap.hs>
-- uses 'Control.Exception.evaluate' instead of 'C.env'.  Is that
-- because of using the @gauge@ library instead of @criterion@, or
-- is there some other reason?
main :: IO ()
main = C.defaultMain
  [ C.env generateEnv $ \ ~(ts, qs) ->
    C.bgroup "MatchOne"
    [ C.bench "match_v027"      $ cartesianNF match_v027  ts qs
    , C.bench "match_foldl"     $ cartesianNF match_foldl ts qs
    , C.bench "match_foldr"     $ cartesianNF match_foldr ts qs
    , C.bench "match_foldr_alt" $ cartesianNF match_foldr_alt ts qs
    ]
  ]

----------------------------------------------------------------
----------------------------------------------------------- fin.