packages feed

croaring-ffi-1.0: bench/Bench/Unfair.hs

{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE NoFieldSelectors #-}

module Bench.Unfair (benchmarks) where

import Bench.Baseline qualified as Baseline
import Bench.Values (dense, scattered)
import Control.Exception (bracket)
import Data.Binary qualified as Binary
import Data.Bitmap.Roaring.FFI.Bitmap (Bitmap)
import Data.Bitmap.Roaring.FFI.Bitmap qualified as Bitmap
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.ByteString.Lazy qualified as Lazy
import Data.IntSet (IntSet)
import Data.IntSet qualified as IntSet
import Data.Word (Word32, Word64)
import Foreign.C.Types (CChar, CSize)
import Foreign.Marshal.Alloc (alloca, allocaBytes, free, mallocBytes)
import Foreign.Marshal.Array (allocaArray, peekArray, withArrayLen)
import Foreign.Ptr (Ptr)
import Foreign.Storable (peek)
import Test.Tasty (withResource)
import Test.Tasty.Bench (Benchmark, Benchmarkable, bgroup, env, nf, nfIO, whnf, whnfIO)

data Dataset = Dataset
  { name :: String
  , left :: [Word32]
  , right :: [Word32]
  , low :: Word32
  , high :: Word32
  }

data Contender = Contender
  { name :: String
  , difference :: Benchmarkable
  , intersectionSize :: Benchmarkable
  , jaccard :: Benchmarkable
  , rangeCount :: Benchmarkable
  , rank :: Benchmarkable
  , page :: Benchmarkable
  , shift :: Benchmarkable
  , serialize :: Benchmarkable
  , deserialize :: Benchmarkable
  , view :: Benchmarkable
  }

size :: Int
size = 1_000_000

pageOffset :: Int
pageOffset = size `div` 2

pageSize :: Int
pageSize = 100

offset :: Int
offset = 1_000_000

benchmarks :: Benchmark
benchmarks =
  bgroup "unfair" $
    map
      dataset
      [ Dataset
          { name = "sparse"
          , left = scattered 1 size
          , right = scattered 2 size
          , low = 0x40000000
          , high = 0xC0000000
          }
      , Dataset
          { name = "dense"
          , left = dense size
          , right = map (+ fromIntegral (size `div` 2)) $ dense size
          , low = 250_000
          , high = 750_000
          }
      ]

dataset :: Dataset -> Benchmark
dataset spec =
  withIntSet spec $ \intSet ->
    withBitmap spec $ \bitmap ->
      withBitmap64 spec $ \bitmap64 ->
        bgroup spec.name $ contests spec.name intSet [bitmap, bitmap64]

contests :: String -> Contender -> [Contender] -> [Benchmark]
contests datasetName baseline others =
  [ versus "difference" (.difference)
  , versus "intersection-size" (.intersectionSize)
  , versus "jaccard" (.jaccard)
  , versus "range-count" (.rangeCount)
  , versus "rank" (.rank)
  , versus "page" (.page)
  , versus "shift" (.shift)
  , versus "serialize" (.serialize)
  , versus "deserialize" (.deserialize)
  , versus "view" (.view)
  ]
  where
    versus = Baseline.versus [datasetName, "unfair"] baseline others

middle :: Dataset -> Word32
middle spec = spec.low + (spec.high - spec.low) `div` 2

withIntSet :: Dataset -> (Contender -> Benchmark) -> Benchmark
withIntSet spec continue =
  env (pure (left, right, Binary.encode left)) $ \ ~(intSet, other, encoded) ->
    continue
      Contender
        { name = "IntSet"
        , difference = nf (IntSet.difference intSet) other
        , intersectionSize = whnf (IntSet.size . IntSet.intersection intSet) other
        , jaccard = whnf (jaccardOf intSet) other
        , rangeCount = whnf rangeCountOf intSet
        , rank = whnf rankOf intSet
        , page = nf pageOf intSet
        , shift = nf (IntSet.mapMonotonic (+ offset)) intSet
        , serialize = nf Binary.encode intSet
        , deserialize = nf decode encoded
        , view = nf decode encoded
        }
  where
    left = IntSet.fromList $ map fromIntegral spec.left
    right = IntSet.fromList $ map fromIntegral spec.right
    decode :: Lazy.ByteString -> IntSet
    decode = Binary.decode
    jaccardOf one two = fromIntegral common / fromIntegral (IntSet.size one + IntSet.size two - common) :: Double
      where
        common = IntSet.size $ IntSet.intersection one two
    rangeCountOf set = IntSet.size inside
      where
        (_, above) = IntSet.split (fromIntegral spec.low - 1) set
        (inside, _) = IntSet.split (fromIntegral spec.high) above
    rankOf set = IntSet.size below + fromEnum found
      where
        (below, found, _) = IntSet.splitMember (fromIntegral $ middle spec) set
    pageOf = take pageSize . drop pageOffset . IntSet.toAscList

withBitmap :: Dataset -> (Contender -> Benchmark) -> Benchmark
withBitmap spec continue =
  withResource (bitmapOf spec.left) Bitmap.free $ \getLeft ->
    withResource (bitmapOf spec.right) Bitmap.free $ \getRight ->
      withResource (getLeft >>= serialized) (free . fst) $ \getBytes ->
        let both operation = do
              left <- getLeft
              right <- getRight
              operation left right
        in continue
             Contender
               { name = "Bitmap"
               , difference = whnfIO $ both Bitmap.andnot >>= Bitmap.free
               , intersectionSize = whnfIO $ both Bitmap.andCardinality
               , jaccard = whnfIO $ both Bitmap.jaccardIndex
               , rangeCount = whnfIO $ getLeft >>= \bitmap -> Bitmap.rangeCardinality bitmap (fromIntegral spec.low) $ fromIntegral spec.high
               , rank = whnfIO $ getLeft >>= \bitmap -> Bitmap.rank bitmap $ middle spec
               , page = nfIO $ getLeft >>= pageOf
               , shift = whnfIO $ getLeft >>= \bitmap -> Bitmap.addOffset bitmap (fromIntegral offset) >>= Bitmap.free
               , serialize = whnfIO $ getLeft >>= serializeInto
               , deserialize = whnfIO $ getBytes >>= \(bytes, count) -> Bitmap.portableDeserializeBounded bytes count >>= Bitmap.free
               , view = whnfIO $ getBytes >>= \(bytes, _) -> Bitmap.portableDeserializeFrozen bytes >>= Bitmap.free
               }
  where
    serialized bitmap = do
      count <- Bitmap.portableSizeInBytes bitmap
      bytes <- mallocBytes $ fromIntegral count
      written <- Bitmap.portableSerialize bitmap bytes
      pure (bytes, written)
    serializeInto bitmap = do
      count <- Bitmap.portableSizeInBytes bitmap
      allocaBytes (fromIntegral count) $ Bitmap.portableSerialize bitmap
    pageOf bitmap =
      allocaArray pageSize $ \buffer -> do
        _ <- Bitmap.rangeUint32Array bitmap (fromIntegral pageOffset) (fromIntegral pageSize) buffer
        peekArray pageSize buffer

bitmapOf :: [Word32] -> IO Bitmap
bitmapOf values = withArrayLen values $ \count ptr -> Bitmap.ofPtr (fromIntegral count) ptr

withBitmap64 :: Dataset -> (Contender -> Benchmark) -> Benchmark
withBitmap64 spec continue =
  withResource (bitmap64Of spec.left) Bitmap64.free $ \getLeft ->
    withResource (bitmap64Of spec.right) Bitmap64.free $ \getRight ->
      withResource (getLeft >>= serialized) (free . fst) $ \getBytes ->
        let both operation = do
              left <- getLeft
              right <- getRight
              operation left right
        in continue
             Contender
               { name = "Bitmap64"
               , difference = whnfIO $ both Bitmap64.andnot >>= Bitmap64.free
               , intersectionSize = whnfIO $ both Bitmap64.andCardinality
               , jaccard = whnfIO $ both Bitmap64.jaccardIndex
               , rangeCount = whnfIO $ getLeft >>= \bitmap -> Bitmap64.rangeCardinality bitmap (fromIntegral spec.low) $ fromIntegral spec.high
               , rank = whnfIO $ getLeft >>= \bitmap -> Bitmap64.rank bitmap . fromIntegral $ middle spec
               , page = nfIO $ getLeft >>= pageOf
               , shift = whnfIO $ getLeft >>= \bitmap -> Bitmap64.addOffset bitmap (fromIntegral offset) >>= Bitmap64.free
               , serialize = whnfIO $ getLeft >>= serializeInto
               , deserialize = whnfIO $ getBytes >>= \(bytes, count) -> Bitmap64.portableDeserializeBounded bytes count >>= Bitmap64.free
               , view = whnfIO $ getBytes >>= \(bytes, count) -> Bitmap64.portableDeserializeFrozen bytes count >>= Bitmap64.free
               }
  where
    serialized :: Bitmap64 -> IO (Ptr CChar, CSize)
    serialized bitmap = do
      count <- Bitmap64.portableSizeInBytes bitmap
      bytes <- mallocBytes $ fromIntegral count
      written <- Bitmap64.portableSerialize bitmap bytes
      pure (bytes, written)
    serializeInto bitmap = do
      count <- Bitmap64.portableSizeInBytes bitmap
      allocaBytes (fromIntegral count) $ Bitmap64.portableSerialize bitmap
    pageOf :: Bitmap64 -> IO [Word64]
    pageOf bitmap =
      alloca $ \start -> do
        _ <- Bitmap64.select bitmap (fromIntegral pageOffset) start
        first <- peek start
        bracket (Iterator64.create bitmap) Iterator64.free $ \iterator ->
          allocaArray pageSize $ \buffer -> do
            _ <- Iterator64.moveEqualOrLarger iterator first
            count <- Iterator64.read iterator buffer $ fromIntegral pageSize
            peekArray (fromIntegral count) buffer

bitmap64Of :: [Word32] -> IO Bitmap64
bitmap64Of values = withArrayLen (map fromIntegral values) $ \count ptr -> Bitmap64.ofPtr (fromIntegral count) ptr