croaring-ffi-1.0: bench/Bench/Comparison.hs
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE NoFieldSelectors #-}
module Bench.Comparison
( benchmarks
, scaling
) where
import Bench.Baseline qualified as Baseline
import Bench.Values (dense, scattered)
import Control.DeepSeq (NFData)
import Control.Exception (bracket)
import Data.Bit (Bit (..), countBits, listBits, zipBits)
import Data.Bitmap.Roaring.FFI.Bitmap (Bitmap)
import Data.Bitmap.Roaring.FFI.Bitmap qualified as Bitmap
import Data.Bitmap.Roaring.FFI.Bitmap.Iterator (Iterator (..))
import Data.Bitmap.Roaring.FFI.Bitmap.Iterator qualified as Iterator
import Data.Bitmap.Roaring.FFI.Bitmap64 (Bitmap64)
import Data.Bitmap.Roaring.FFI.Bitmap64 qualified as Bitmap64
import Data.Bitmap.Roaring.FFI.Bitmap64.Iterator qualified as Iterator64
import Data.Bitmap.Roaring.FFI.Bitset (Bitset)
import Data.Bitmap.Roaring.FFI.Bitset qualified as Bitset
import Data.Bits ((.&.), (.|.))
import Data.IntSet (IntSet)
import Data.IntSet qualified as IntSet
import Data.Vector.Storable qualified as Storable
import Data.Vector.Unboxed qualified as Unboxed
import Data.Vector.Unboxed.Mutable qualified as Unboxed.Mutable
import Data.Word (Word32, Word64)
import Foreign.C.Types (CSize)
import Foreign.Marshal.Alloc (alloca, allocaBytesAligned)
import Foreign.Marshal.Array (allocaArray)
import Foreign.Ptr (Ptr)
import Foreign.Storable (Storable, peek, peekElemOff, poke)
import GHC.Generics (Generic)
import Test.Tasty (withResource)
import Test.Tasty.Bench (Benchmark, Benchmarkable, bgroup, env, nf, whnf, whnfIO)
data Dataset = Dataset
{ name :: String
, universe :: Maybe Int
, left :: [Word32]
, right :: [Word32]
}
data Inputs = Inputs
{ left :: Storable.Vector Word32
, right :: Storable.Vector Word32
, probes :: Storable.Vector Word32
, left64 :: Storable.Vector Word64
, right64 :: Storable.Vector Word64
, probes64 :: Storable.Vector Word64
}
deriving (Generic)
instance NFData Inputs
data Contender = Contender
{ name :: String
, construction :: Benchmarkable
, member :: Benchmarkable
, cardinality :: Benchmarkable
, union :: Benchmarkable
, intersection :: Benchmarkable
, iteration :: Benchmarkable
}
size :: Int
size = 1_000_000
benchmarks :: Benchmark
benchmarks =
bgroup "comparison" $
map
(dataset "comparison")
[ Dataset
{ name = "scattered"
, universe = Nothing
, left = scattered 1 size
, right = scattered 2 size
}
, Dataset
{ name = "bounded"
, universe = Just 0x1000000
, left = map (.&. 0xFFFFFF) $ scattered 1 size
, right = map (.&. 0xFFFFFF) $ scattered 2 size
}
, Dataset
{ name = "dense"
, universe = Just 0x200000
, left = dense size
, right = map (+ fromIntegral (size `div` 2)) $ dense size
}
]
scaling :: Benchmark
scaling =
bgroup "scaling" $ map (dataset "scaling" . sparse) [1_000, 3_000, 10_000, 30_000, 100_000, 300_000, 1_000_000]
where
sparse count =
Dataset
{ name = show count
, universe = Nothing
, left = scattered 1 count
, right = scattered 2 count
}
dataset :: String -> Dataset -> Benchmark
dataset group spec =
env (pure prepared) $ \inputs ->
withIntSet inputs $ \intSet ->
withBitmap inputs $ \bitmap ->
withBitmap64 inputs $ \bitmap64 ->
withBitVectors spec.universe inputs $ \bitVectors ->
bgroup spec.name . contests group spec.name intSet $ bitmap : bitmap64 : bitVectors
where
left = Storable.fromList spec.left
right = Storable.fromList spec.right
probes = Storable.fromList . concat $ zipWith (\hit miss -> [hit, miss]) spec.left spec.right
prepared =
Inputs
{ left
, right
, probes
, left64 = Storable.map fromIntegral left
, right64 = Storable.map fromIntegral right
, probes64 = Storable.map fromIntegral probes
}
contests :: String -> String -> Contender -> [Contender] -> [Benchmark]
contests group datasetName baseline others =
[ versus "construction" (.construction)
, versus "member" (.member)
, versus "cardinality" (.cardinality)
, versus "union" (.union)
, versus "intersection" (.intersection)
, versus "iteration" (.iteration)
]
where
versus = Baseline.versus [datasetName, group] baseline others
withBitmap :: Inputs -> (Contender -> Benchmark) -> Benchmark
withBitmap inputs continue =
withResource (bitmapOf inputs.left) Bitmap.free $ \getLeft ->
withResource (bitmapOf inputs.right) Bitmap.free $ \getRight ->
continue
Contender
{ name = "Bitmap"
, construction = whnfIO $ bitmapOf inputs.left >>= Bitmap.free
, member = whnfIO $ do
bitmap <- getLeft
tallyIO (Bitmap.contains bitmap) inputs.probes
, cardinality = whnfIO $ getLeft >>= Bitmap.getCardinality
, union = whnfIO $ combined getLeft getRight Bitmap.or
, intersection = whnfIO $ combined getLeft getRight Bitmap.and
, iteration = whnfIO $ getLeft >>= sumBitmap
}
where
combined getLeft getRight operation = do
left <- getLeft
right <- getRight
operation left right >>= Bitmap.free
bitmapOf :: Storable.Vector Word32 -> IO Bitmap
bitmapOf values = Storable.unsafeWith values . Bitmap.ofPtr . fromIntegral $ Storable.length values
sumBitmap :: Bitmap -> IO Word64
sumBitmap bitmap =
allocaBytesAligned Iterator.size Iterator.alignment $ \memory ->
allocaArray chunk $ \buffer -> do
let
iterator = Iterator memory
loop !accumulated = do
count <- fromIntegral <$> Iterator.read iterator buffer (fromIntegral chunk)
summed <- sumBuffer buffer count accumulated
if count < chunk then pure summed else loop summed
Iterator.init bitmap iterator
loop 0
where
chunk = 256
withBitmap64 :: Inputs -> (Contender -> Benchmark) -> Benchmark
withBitmap64 inputs continue =
withResource (bitmap64Of inputs.left64) Bitmap64.free $ \getLeft ->
withResource (bitmap64Of inputs.right64) Bitmap64.free $ \getRight ->
continue
Contender
{ name = "Bitmap64"
, construction = whnfIO $ bitmap64Of inputs.left64 >>= Bitmap64.free
, member = whnfIO $ do
bitmap <- getLeft
tallyIO (Bitmap64.contains bitmap) inputs.probes64
, cardinality = whnfIO $ getLeft >>= Bitmap64.getCardinality
, union = whnfIO $ combined getLeft getRight Bitmap64.or
, intersection = whnfIO $ combined getLeft getRight Bitmap64.and
, iteration = whnfIO $ getLeft >>= sumBitmap64
}
where
combined getLeft getRight operation = do
left <- getLeft
right <- getRight
operation left right >>= Bitmap64.free
bitmap64Of :: Storable.Vector Word64 -> IO Bitmap64
bitmap64Of values = Storable.unsafeWith values . Bitmap64.ofPtr . fromIntegral $ Storable.length values
sumBitmap64 :: Bitmap64 -> IO Word64
sumBitmap64 bitmap =
bracket (Iterator64.create bitmap) Iterator64.free $ \iterator ->
allocaArray chunk $ \buffer -> do
let loop !accumulated = do
count <- fromIntegral <$> Iterator64.read iterator buffer (fromIntegral chunk)
summed <- sumBuffer buffer count accumulated
if count < chunk then pure summed else loop summed
loop 0
where
chunk = 256
withIntSet :: Inputs -> (Contender -> Benchmark) -> Benchmark
withIntSet inputs continue =
env (pure (intSetOf inputs.left, intSetOf inputs.right)) $ \ ~(left, right) ->
continue
Contender
{ name = "IntSet"
, construction = nf intSetOf inputs.left
, member = whnf (countMembers inputs.probes) left
, cardinality = whnf IntSet.size left
, union = nf (IntSet.union left) right
, intersection = nf (IntSet.intersection left) right
, iteration = whnf (IntSet.foldl' (+) 0) left
}
where
countMembers probes set = tally isMember probes
where
isMember value = IntSet.member (fromIntegral value) set
intSetOf :: Storable.Vector Word32 -> IntSet
intSetOf = Storable.foldl' (\set value -> IntSet.insert (fromIntegral value) set) IntSet.empty
withBitVectors :: Maybe Int -> Inputs -> ([Contender] -> Benchmark) -> Benchmark
withBitVectors universe inputs continue =
case universe of
Nothing -> continue []
Just bits ->
withBitset bits inputs $ \bitset ->
withBitvec bits inputs $ \bitvec ->
continue [bitset, bitvec]
withBitset :: Int -> Inputs -> (Contender -> Benchmark) -> Benchmark
withBitset universe inputs continue =
withResource (bitsetOf universe inputs.left) Bitset.free $ \getLeft ->
withResource (bitsetOf universe inputs.right) Bitset.free $ \getRight ->
continue
Contender
{ name = "Bitset"
, construction = whnfIO $ bitsetOf universe inputs.left >>= Bitset.free
, member = whnfIO $ do
bitset <- getLeft
tallyIO (Bitset.get bitset . fromIntegral) inputs.probes
, cardinality = whnfIO $ getLeft >>= Bitset.count
, union = whnfIO $ combined getLeft getRight Bitset.inplaceUnion
, intersection = whnfIO $ combined getLeft getRight Bitset.inplaceIntersection
, iteration = whnfIO $ getLeft >>= sumBitset
}
where
combined getLeft getRight operation = do
left <- getLeft
right <- getRight
copy <- Bitset.copy left
_ <- operation copy right
Bitset.free copy
bitsetOf :: Int -> Storable.Vector Word32 -> IO Bitset
bitsetOf universe values = do
bitset <- Bitset.createWithCapacity $ fromIntegral universe
Storable.mapM_ (Bitset.set bitset . fromIntegral) values
pure bitset
sumBitset :: Bitset -> IO CSize
sumBitset bitset =
allocaArray chunk $ \buffer ->
alloca $ \start -> do
poke start 0
let loop !accumulated = do
count <- fromIntegral <$> Bitset.nextSetBits bitset buffer (fromIntegral chunk) start
if count == 0 then
pure accumulated
else do
summed <- sumBuffer buffer count accumulated
peek start >>= poke start . (+ 1)
loop summed
loop 0
where
chunk = 256
withBitvec :: Int -> Inputs -> (Contender -> Benchmark) -> Benchmark
withBitvec universe inputs continue =
env (pure (bitvecOf universe inputs.left, bitvecOf universe inputs.right)) $ \ ~(left, right) ->
continue
Contender
{ name = "bitvec"
, construction = whnf (bitvecOf universe) inputs.left
, member = whnf (countMembers inputs.probes) left
, cardinality = whnf countBits left
, union = whnf (zipBits (.|.) left) right
, intersection = whnf (zipBits (.&.) left) right
, iteration = whnf (foldl' (+) 0 . listBits) left
}
where
countMembers probes bits = tally isSet probes
where
isSet value = unBit . Unboxed.unsafeIndex bits $ fromIntegral value
bitvecOf :: Int -> Storable.Vector Word32 -> Unboxed.Vector Bit
bitvecOf universe values =
Unboxed.create $ do
bits <- Unboxed.Mutable.replicate universe $ Bit False
Storable.forM_ values $ \value -> Unboxed.Mutable.unsafeWrite bits (fromIntegral value) $ Bit True
pure bits
tallyIO :: (Storable a) => (a -> IO Bool) -> Storable.Vector a -> IO Int
tallyIO query values = go 0 0
where
go !hits index
| index == Storable.length values = pure hits
| otherwise = do
hit <- query $ Storable.unsafeIndex values index
go (counted hits hit) $ index + 1
{-# INLINE tallyIO #-}
tally :: (Storable a) => (a -> Bool) -> Storable.Vector a -> Int
tally query values = go 0 0
where
go !hits index
| index == Storable.length values = hits
| otherwise = go (counted hits . query $ Storable.unsafeIndex values index) $ index + 1
{-# INLINE tally #-}
counted :: Int -> Bool -> Int
counted hits hit = if hit then hits + 1 else hits
{-# INLINE counted #-}
sumBuffer :: (Storable a, Integral a, Num total) => Ptr a -> Int -> total -> IO total
sumBuffer buffer count = go 0
where
go index !accumulated
| index == count = pure accumulated
| otherwise = do
value <- peekElemOff buffer index
go (index + 1) $ accumulated + fromIntegral value
{-# INLINE sumBuffer #-}