packages feed

pure-borrow-0.1.0.0: bench/suite/PureBorrow/Bench/HashMap.hs

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE QualifiedDo #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# OPTIONS_GHC -Wno-name-shadowing #-}

{- | Benchmarks of the Robin Hood hash table.

Each group measures the same workload three ways: this package's table (owned,
and through its borrow-aware wrapper), @linear-base@'s own linear hash map, and
the unrestricted @unordered-containers@ map. The expensive-key groups exist
because the table caches a fingerprint per slot: a key whose hash is cheap but
whose equality is not is exactly where that cache pays off, and the colliding
group is the extreme of that, where every key lands in one bucket and only the
fingerprint can reject a candidate without a full comparison.
-}
module PureBorrow.Bench.HashMap (
  test_hashMap,
  ExpensiveKey (..),
  benchInsertRobinHood,
  benchInsertRobinHoodBorrow,
  benchInsertLinearBase,
  benchInsertUnordered,
  benchFromListRobinHood,
  benchFromListLinearBase,
  benchFromListUnordered,
  benchLookupRobinHood,
  benchLookupRobinHoodBorrow,
  benchLookupLinearBase,
  benchLookupUnordered,
  testData,
  expensiveTestData,
  collidingExpensiveTestData,
) where

import Control.DeepSeq (NFData, force)
import Control.Exception (evaluate)
import Control.Functor.Linear qualified as Control
import Control.Monad.Borrow.Pure
import Control.Syntax.DataFlow qualified as DataFlow
import Data.HashMap.Mutable.Linear qualified as LB
import Data.HashMap.RobinHood.Mutable.Linear qualified as RH
import Data.HashMap.RobinHood.Mutable.Linear.Borrow qualified as RHB
import Data.HashMap.Strict qualified as UCHM
import Data.Hashable (Hashable (..))
import GHC.Generics (Generic)
import Prelude.Linear (lseq, unur, (&))
import Prelude.Linear qualified as PL
import Test.Tasty.Bench

{- | A key with a cheap hash and an expensive equality.

The hash reads only the prefix, so two keys sharing a prefix collide and force
the payloads to be compared.
-}
data ExpensiveKey = ExpensiveKey
  { ekPrefix :: {-# UNPACK #-} !Int
  , ekPayload :: ![Int]
  }
  deriving (Show, Generic, NFData)

instance Eq ExpensiveKey where
  ExpensiveKey p1 payload1 == ExpensiveKey p2 payload2 =
    p1 == p2 && payload1 == payload2
  {-# INLINE (==) #-}

instance Hashable ExpensiveKey where
  hashWithSalt s (ExpensiveKey p _) = hashWithSalt s p
  {-# INLINE hashWithSalt #-}

-- * Insertion into an empty table

benchInsertRobinHood :: (Hashable k) => [(k, v)] -> [(k, v)]
benchInsertRobinHood kvs = unur PL.$ linearly \lin ->
  let %1 !hm = RH.new (length kvs) lin
   in RH.toList (RH.insertMany kvs hm)

benchInsertRobinHoodBorrow :: (Hashable k) => [(k, v)] -> [(k, v)]
benchInsertRobinHoodBorrow kvs = unur PL.$ linearly \lin -> DataFlow.do
  (ownerLinear, runLinear) <- PL.dup lin
  runBO runLinear Control.do
    (table, lend) <- borrowM (RHB.empty (length kvs) ownerLinear)
    table <- insertAllBorrow kvs table
    (Ur entries, table) <- RHB.toList table
    PL.consume table `lseq`
      pureAfter (PL.consume (reclaim lend) `lseq` Ur entries)

insertAllBorrow ::
  (Hashable k) =>
  [(k, v)] ->
  Mut α (RHB.HashMap k v) %1 ->
  BO α (Mut α (RHB.HashMap k v))
insertAllBorrow [] table = Control.pure table
insertAllBorrow ((k, v) : rest) table = Control.do
  (Ur _, table) <- RHB.insert k v table
  insertAllBorrow rest table

benchInsertLinearBase :: (LB.Keyed k) => [(k, v)] -> [(k, v)]
benchInsertLinearBase kvs = unur PL.$ LB.empty (length kvs) \hm ->
  LB.toList (insertManyLinearBase kvs hm)

insertManyLinearBase :: (LB.Keyed k) => [(k, v)] -> LB.HashMap k v %1 -> LB.HashMap k v
insertManyLinearBase [] hm = hm
insertManyLinearBase ((k, v) : rest) hm = insertManyLinearBase rest (LB.insert k v hm)

benchInsertUnordered :: (Hashable k) => [(k, v)] -> [(k, v)]
benchInsertUnordered kvs = UCHM.toList (foldr (uncurry UCHM.insert) UCHM.empty kvs)

-- * Bulk construction

benchFromListRobinHood :: (Hashable k) => [(k, v)] -> [(k, v)]
benchFromListRobinHood kvs = unur PL.$ linearly \lin ->
  RH.toList (RH.fromList kvs lin)

benchFromListLinearBase :: (LB.Keyed k) => [(k, v)] -> [(k, v)]
benchFromListLinearBase kvs = unur PL.$ LB.fromList kvs LB.toList

benchFromListUnordered :: (Hashable k) => [(k, v)] -> [(k, v)]
benchFromListUnordered kvs = UCHM.toList (UCHM.fromList kvs)

-- * Lookup after a bulk insertion

benchLookupRobinHood :: forall k v. (Hashable k) => [(k, v)] -> [k] -> Int
benchLookupRobinHood kvs keys = unur PL.$ linearly \lin ->
  let %1 !hm = RH.insertMany kvs (RH.new (length kvs) lin)
   in go 0 keys hm
  where
    go :: Int -> [k] -> RH.HashMap k v %1 -> Ur Int
    go !acc [] hm = hm `lseq` Ur acc
    go !acc (k : ks) hm =
      RH.lookup k hm & \(Ur mv, hm') ->
        go (acc + maybe 0 (const 1) mv) ks hm'

benchLookupRobinHoodBorrow :: forall k v. (Hashable k) => [(k, v)] -> [k] -> Int
benchLookupRobinHoodBorrow kvs keys = unur PL.$ linearly \lin -> DataFlow.do
  (ownerLinear, runLinear) <- PL.dup lin
  runBO runLinear Control.do
    (table, lend) <- borrowM (RHB.fromList kvs ownerLinear)
    (Ur found, table) <- go 0 keys table
    PL.consume table `lseq`
      pureAfter (PL.consume (reclaim lend) `lseq` Ur found)
  where
    go ::
      Int ->
      [k] ->
      Mut α (RHB.HashMap k v) %1 ->
      BO α (Ur Int, Mut α (RHB.HashMap k v))
    go !acc [] table = Control.pure (Ur acc, table)
    go !acc (k : ks) table = Control.do
      (Ur mv, table) <- RHB.lookup k table
      go (acc + maybe 0 (const 1) mv) ks table

benchLookupLinearBase :: forall k v. (LB.Keyed k) => [(k, v)] -> [k] -> Int
benchLookupLinearBase kvs keys = unur PL.$ LB.empty (length kvs) \hm ->
  let %1 !hm' = insertManyLinearBase kvs hm
   in go 0 keys hm'
  where
    go :: Int -> [k] -> LB.HashMap k v %1 -> Ur Int
    go !acc [] hm = hm `lseq` Ur acc
    go !acc (k : ks) hm =
      LB.lookup k hm & \(Ur mv, hm') ->
        go (acc + maybe 0 (const 1) mv) ks hm'

benchLookupUnordered :: forall k v. (Hashable k) => [(k, v)] -> [k] -> Int
benchLookupUnordered kvs keys = go 0 keys (foldr (uncurry UCHM.insert) UCHM.empty kvs)
  where
    go !acc [] _ = acc
    go !acc (k : ks) hm =
      go (acc + maybe 0 (const 1) (UCHM.lookup k hm)) ks hm

-- * Inputs

-- | Distinct integer keys.
testData :: Int -> [(Int, Int)]
testData n = [(i, i) | i <- [1 .. n]]

-- | Distinct expensive keys: unique prefixes, hundred-element payloads.
expensiveTestData :: Int -> [(ExpensiveKey, Int)]
expensiveTestData n =
  [ (ExpensiveKey i [i .. i + 99], i)
  | i <- [1 .. n]
  ]

-- | Expensive keys that all hash to one bucket.
collidingExpensiveTestData :: Int -> [(ExpensiveKey, Int)]
collidingExpensiveTestData n =
  [ (ExpensiveKey 42 [i .. i + 99], i)
  | i <- [1 .. n]
  ]

test_hashMap :: [Benchmark]
test_hashMap =
  [ bgroup
      "hashmap/insert"
      [ env (evaluate (force (testData n))) \kvs ->
          bgroup
            (show n)
            [ bench "robin-hood" $ nf benchInsertRobinHood kvs
            , bench "robin-hood/borrow" $ nf benchInsertRobinHoodBorrow kvs
            , bench "linear-base" $ nf benchInsertLinearBase kvs
            , bench "unordered-containers" $ nf benchInsertUnordered kvs
            ]
      | n <- sizes
      ]
  , bgroup
      "hashmap/from-list"
      [ env (evaluate (force (testData n))) \kvs ->
          bgroup
            (show n)
            [ bench "robin-hood" $ nf benchFromListRobinHood kvs
            , bench "linear-base" $ nf benchFromListLinearBase kvs
            , bench "unordered-containers" $ nf benchFromListUnordered kvs
            ]
      | n <- sizes
      ]
  , bgroup
      "hashmap/lookup"
      [ env (evaluate (force (testData n))) \kvs ->
          bgroup
            (show n)
            [ bench "robin-hood" $ nf (benchLookupRobinHood kvs) (map fst kvs)
            , bench "robin-hood/borrow" $ nf (benchLookupRobinHoodBorrow kvs) (map fst kvs)
            , bench "linear-base" $ nf (benchLookupLinearBase kvs) (map fst kvs)
            , bench "unordered-containers" $ nf (benchLookupUnordered kvs) (map fst kvs)
            ]
      | n <- sizes
      ]
  , bgroup
      "hashmap/expensive-key-insert"
      [ env (evaluate (force (expensiveTestData n))) \kvs ->
          bgroup
            (show n)
            [ bench "robin-hood" $ nf benchInsertRobinHood kvs
            , bench "linear-base" $ nf benchInsertLinearBase kvs
            , bench "unordered-containers" $ nf benchInsertUnordered kvs
            ]
      | n <- sizes
      ]
  , bgroup
      "hashmap/expensive-key-lookup"
      [ env (evaluate (force (expensiveTestData n))) \kvs ->
          bgroup
            (show n)
            [ bench "robin-hood" $ nf (benchLookupRobinHood kvs) (map fst kvs)
            , bench "linear-base" $ nf (benchLookupLinearBase kvs) (map fst kvs)
            , bench "unordered-containers" $ nf (benchLookupUnordered kvs) (map fst kvs)
            ]
      | n <- sizes
      ]
  , bgroup
      "hashmap/colliding-expensive-key-lookup"
      [ env (evaluate (force (collidingExpensiveTestData n))) \kvs ->
          bgroup
            (show n)
            [ bench "robin-hood" $ nf (benchLookupRobinHood kvs) (map fst kvs)
            , bench "linear-base" $ nf (benchLookupLinearBase kvs) (map fst kvs)
            , bench "unordered-containers" $ nf (benchLookupUnordered kvs) (map fst kvs)
            ]
      | -- Every key lands in one bucket, so the workload is quadratic; the
      -- large size would dominate the whole suite's wall clock.
      n <- collidingSizes
      ]
  ]

sizes :: [Int]
sizes = [100, 1000, 10000]

collidingSizes :: [Int]
collidingSizes = [100, 1000]