bloomfilter-blocked-0.1.0.0: src/Data/BloomFilter/Blocked/BitArray.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE UnboxedTuples #-}
-- | Blocked bit array implementation. This uses blocks of 64 bytes, aligned
-- to 64byte boundaries to match typical cache line sizes. This means that
-- multiple accesses to the same block only require a single cache line load
-- or store.
module Data.BloomFilter.Blocked.BitArray (
NumBlocks (..),
bitsToBlocks,
blocksToBits,
BlockIx (..),
BitIx (..),
BitArray (..),
unsafeIndex,
prefetchIndex,
MBitArray (..),
new,
unsafeSet,
prefetchSet,
unsafeRead,
freeze,
unsafeFreeze,
thaw,
serialise,
deserialise,
) where
import Control.Exception (assert)
import Control.Monad.Primitive (PrimMonad, PrimState)
import Control.Monad.ST (ST)
import Data.Bits
import Data.Primitive.ByteArray
import Data.Primitive.PrimArray
import Data.Word (Word64, Word8)
import GHC.Exts (Int (I#), prefetchByteArray0#,
prefetchMutableByteArray0#)
import GHC.ST (ST (ST))
-- | An array of blocks of bits.
--
-- Each block is 512 bits (64 bytes large), corresponding to a cache line on
-- most current architectures.
--
-- It is represented by an array of 'Word64'. This array is aligned to 64 bytes
-- so that multiple accesses within a single block will use only one cache line.
--
newtype BitArray = BitArray (PrimArray Word64)
deriving stock (Eq, Show)
-- | Blocks are 512 bits, 64 bytes.
newtype NumBlocks = NumBlocks Int
deriving stock Eq
-- | The number of 512-bit blocks for the given number of bits. This rounds
-- up to the nearest multiple of 512.
bitsToBlocks :: Int -> NumBlocks
bitsToBlocks n = NumBlocks ((n+511) `div` 512) -- rounded up
blocksToBits :: NumBlocks -> Int
blocksToBits (NumBlocks n) = n * 512
newtype BlockIx = BlockIx Word
newtype BitIx = BitIx Int
{-# INLINE unsafeIndex #-}
unsafeIndex :: BitArray -> BlockIx -> BitIx -> Bool
unsafeIndex (BitArray arr) blockIx blockBitIx =
assert (wordIx >= 0 && wordIx < sizeofPrimArray arr) $
indexPrimArray arr wordIx `unsafeTestBit` wordBitIx
where
(wordIx, wordBitIx) = wordAndBitIndex blockIx blockBitIx
{-# INLINE prefetchIndex #-}
prefetchIndex :: BitArray -> BlockIx -> ST s ()
prefetchIndex (BitArray (PrimArray ba#)) (BlockIx blockIx) =
-- For reading, we want to prefetch such that we do least disturbance of
-- the caches. We will typically not keep this cache line longer than one
-- use of elemHashes which does several memory reads of the same cache line.
let !i@(I# i#) = fromIntegral blockIx `shiftL` 6 in
-- blockIx * 64 to go from block index to the byte offset of the beginning
-- of the block. This offset is in bytes, not words.
assert (i >= 0 && i < sizeofByteArray (ByteArray ba#) - 63) $
-- In prefetchByteArray0, the 0 refers to a "non temporal" load, which is
-- a hint that the value will be used soon, and then not used again (soon).
-- So the caches can evict the value as soon as they like.
ST (\s -> case prefetchByteArray0# ba# i# s of
s' -> (# s', () #))
newtype MBitArray s = MBitArray (MutablePrimArray s Word64)
-- | We create an explicitly pinned byte array, aligned to 64 bytes.
--
new :: NumBlocks -> ST s (MBitArray s)
new (NumBlocks numBlocks) = do
mba@(MutableByteArray mba#) <- newAlignedPinnedByteArray numBytes 64
setByteArray mba 0 numBytes (0 :: Word8)
pure (MBitArray (MutablePrimArray mba#))
where
!numBytes = numBlocks * 64
serialise :: BitArray -> (ByteArray, Int, Int)
serialise bitArray =
let ba = asByteArray bitArray
in (ba, 0, sizeofByteArray ba)
where
asByteArray (BitArray (PrimArray ba#)) = ByteArray ba#
{-# INLINE deserialise #-}
-- | Do an inplace overwrite of the byte array representing the bit block.
deserialise :: PrimMonad m
=> MBitArray (PrimState m)
-> (MutableByteArray (PrimState m) -> Int -> Int -> m ())
-> m ()
deserialise bitArray fill = do
let mba = asMutableByteArray bitArray
len <- getSizeofMutableByteArray mba
fill mba 0 len
where
asMutableByteArray (MBitArray (MutablePrimArray mba#)) =
MutableByteArray mba#
unsafeSet :: MBitArray s -> BlockIx -> BitIx -> ST s ()
unsafeSet (MBitArray arr) blockIx blockBitIx = do
#ifdef NO_IGNORE_ASSERTS
sz <- getSizeofMutablePrimArray arr
assert (wordIx >= 0 && wordIx < sz) $ pure ()
#endif
w <- readPrimArray arr wordIx
writePrimArray arr wordIx (unsafeSetBit w wordBitIx)
where
(wordIx, wordBitIx) = wordAndBitIndex blockIx blockBitIx
{-# INLINE prefetchSet #-}
prefetchSet :: MBitArray s -> BlockIx -> ST s ()
prefetchSet (MBitArray (MutablePrimArray mba#)) (BlockIx blockIx) = do
-- For setting, we will do several writes to the same cache line, but all
-- immediately after each other, after which we will not need the value in
-- the cache again (for a long time). So as with prefetchIndex we want to
-- disturbe the caches the least, and so we use prefetchMutableByteArray0.
let !(I# i#) = fromIntegral blockIx `shiftL` 6
-- blockIx * 64 to go from block index to the byte offset of the beginning
-- of the block. This offset is in bytes, not words.
#ifdef NO_IGNORE_ASSERTS
sz <- getSizeofMutableByteArray (MutableByteArray mba#)
assert (let i = I# i# in i >= 0 && i < sz-63) $ pure ()
#endif
-- In prefetchMutableByteArray0, the 0 refers to a "non temporal" load,
-- which is a hint that the value will be used soon, and then not used
-- again (soon). So the caches can evict the value as soon as they like.
ST (\s -> case prefetchMutableByteArray0# mba# i# s of
s' -> (# s', () #))
unsafeRead :: MBitArray s -> BlockIx -> BitIx -> ST s Bool
unsafeRead (MBitArray arr) blockIx blockBitIx = do
#ifdef NO_IGNORE_ASSERTS
sz <- getSizeofMutablePrimArray arr
assert (wordIx >= 0 && wordIx < sz) $ pure ()
#endif
w <- readPrimArray arr wordIx
pure $ unsafeTestBit w wordBitIx
where
(wordIx, wordBitIx) = wordAndBitIndex blockIx blockBitIx
freeze :: MBitArray s -> ST s BitArray
freeze (MBitArray arr) = do
len <- getSizeofMutablePrimArray arr
BitArray <$> freezePrimArray arr 0 len
unsafeFreeze :: MBitArray s -> ST s BitArray
unsafeFreeze (MBitArray arr) =
BitArray <$> unsafeFreezePrimArray arr
thaw :: BitArray -> ST s (MBitArray s)
thaw (BitArray arr) =
MBitArray <$> thawPrimArray arr 0 (sizeofPrimArray arr)
{-# INLINE wordAndBitIndex #-}
-- | Given the index of the 512 bit block, and the index of the bit within the
-- block, compute the index of the word in the array, and index of the bit
-- within the word.
--
wordAndBitIndex :: BlockIx -> BitIx -> (Int, Int)
wordAndBitIndex (BlockIx blockIx) (BitIx blockBitIx) =
assert (blockBitIx < 512) $
(wordIx, wordBitIx)
where
-- Select the Word64 in the underlying array based on the block index
-- and the bit index.
-- * There are 8 Word64s in each 64byte block.
-- * Use 3 bits (bits 6..8) to select the Word64 within the block
wordIx = fromIntegral blockIx `shiftL` 3 -- * 8
+ (blockBitIx `shiftR` 6) .&. 7 -- `div` 64, `mod` 8
-- Bits 0..5 of blockBitIx select the bit within Word64
wordBitIx = blockBitIx .&. 63 -- `mod` 64
{-# INLINE unsafeTestBit #-}
-- like testBit but using unsafeShiftL instead of shiftL
unsafeTestBit :: Word64 -> Int -> Bool
unsafeTestBit w k = w .&. (1 `unsafeShiftL` k) /= 0
{-# INLINE unsafeSetBit #-}
-- like setBit but using unsafeShiftL instead of shiftL
unsafeSetBit :: Word64 -> Int -> Word64
unsafeSetBit w k = w .|. (1 `unsafeShiftL` k)