bloomfilter-blocked-0.1.0.0: src/Data/BloomFilter/Blocked/Internal.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE UnboxedTuples #-}
{-# OPTIONS_HADDOCK not-home #-}
-- | This module defines the 'Bloom' and 'MBloom' types and all the functions
-- that need direct knowledge of and access to the representation. This forms
-- the trusted base.
module Data.BloomFilter.Blocked.Internal (
-- * Mutable Bloom filters
MBloom (mbHashSalt),
new,
maxSizeBits,
-- * Immutable Bloom filters
Bloom (hashSalt),
bloomInvariant,
size,
-- * Hash-based operations
Hashes,
Salt,
hashesWithSalt,
insertHashes,
prefetchInsert,
elemHashes,
prefetchElem,
readHashes,
-- * Conversion
freeze,
unsafeFreeze,
thaw,
-- * (De)Serialisation
formatVersion,
serialise,
deserialise,
) where
import Control.DeepSeq (NFData (..))
import Control.Exception (assert)
import Control.Monad.Primitive (PrimMonad, PrimState)
import Control.Monad.ST (ST)
import Data.Bits
import Data.Kind (Type)
import Data.Primitive.ByteArray
import Data.Primitive.PrimArray
import Data.Primitive.Types (Prim (..))
import Data.BloomFilter.Blocked.BitArray (BitArray, BitIx (..),
BlockIx (..), MBitArray, NumBlocks (..), bitsToBlocks,
blocksToBits)
import qualified Data.BloomFilter.Blocked.BitArray as BitArray
import Data.BloomFilter.Blocked.Calc
import Data.BloomFilter.Hash
-- | The version of the format used by 'serialise' and 'deserialise'. The
-- format number will change when there is an incompatible change in the
-- library, such that deserialising and using the filter will not work.
-- This can include more than just changes to the serialised format, for
-- example changes to hash functions or how the hash is mapped to bits.
--
-- Note that the format produced does not include this version. Version
-- checking is the responsibility of the user of the library.
--
-- The library guarantes that the format version value for the classic
-- ("Data.BloomFilter.Classic") and blocked ("Data.BloomFilter.Blocked")
-- implementation will not overlap with each other or any previous value used
-- by either implementation. So switching between the two implementations will
-- always be detectable and unambigious.
--
-- History:
--
-- * Version 1000: original blocked implementation
--
formatVersion :: Int
formatVersion = 1000
-------------------------------------------------------------------------------
-- Mutable Bloom filters
--
type MBloom :: Type -> Type -> Type
-- | A mutable Bloom filter, for use within the 'ST' monad.
data MBloom s a = MBloom {
mbNumBlocks :: {-# UNPACK #-} !NumBlocks -- ^ non-zero
, mbNumHashes :: {-# UNPACK #-} !Int
, mbHashSalt :: {-# UNPACK #-} !Salt
, mbBitArray :: {-# UNPACK #-} !(MBitArray s)
}
type role MBloom nominal nominal
instance Show (MBloom s a) where
show mb = "MBloom { " ++ show numBits ++ " bits } "
where
numBits = blocksToBits (mbNumBlocks mb)
instance NFData (MBloom s a) where
rnf !_ = ()
-- | Create a new mutable Bloom filter.
--
-- The filter size is capped at 'maxSizeBits'.
--
new :: BloomSize -> Salt -> ST s (MBloom s a)
new BloomSize { sizeBits, sizeHashes } mbHashSalt = do
let numBlocks = bitsToBlocks (max 1 (min maxSizeBits sizeBits))
mbBitArray <- BitArray.new numBlocks
pure MBloom {
mbNumBlocks = numBlocks,
mbNumHashes = max 1 sizeHashes,
mbHashSalt,
mbBitArray
}
-- | The maximum size is @2^41@ bits (256 gigabytes). Tell us if you need bigger
-- bloom filters.
--
-- The reason for the current limit of @2^41@ bits is that this corresponds to
-- @2^32@ blocks, each of size 64 bytes (512 bits). The reason for the current
-- limit of @2^32@ blocks is that for efficiency we use a single 64bit hash per
-- element, and split that into a pair of 32bit hashes which are used for
-- probing the filter. To go bigger would need a pair of hashes.
--
maxSizeBits :: Int
maxSizeBits = 0x200_0000_0000
{-# NOINLINE insertHashes #-}
insertHashes :: forall s a. MBloom s a -> Hashes a -> ST s ()
insertHashes MBloom { mbNumBlocks, mbNumHashes, mbBitArray } !h =
go g0 mbNumHashes
where
blockIx :: BlockIx
(!blockIx, !g0) = blockIxAndBitGen h mbNumBlocks
go :: BitIxGen -> Int -> ST s ()
go !_ 0 = pure ()
go !g !i = do
let blockBitIx :: BitIx
(!blockBitIx, !g') = genBitIndex g
assert (let BlockIx b = blockIx
NumBlocks nb = mbNumBlocks
in b >= 0 && b < fromIntegral nb) $
BitArray.unsafeSet mbBitArray blockIx blockBitIx
go g' (i-1)
prefetchInsert :: MBloom s a -> Hashes a -> ST s ()
prefetchInsert MBloom { mbNumBlocks, mbBitArray } !h =
BitArray.prefetchSet mbBitArray blockIx
where
blockIx :: BlockIx
(!blockIx, _) = blockIxAndBitGen h mbNumBlocks
readHashes :: forall s a. MBloom s a -> Hashes a -> ST s Bool
readHashes MBloom { mbNumBlocks, mbNumHashes, mbBitArray } !h =
go g0 mbNumHashes
where
blockIx :: BlockIx
(!blockIx, !g0) = blockIxAndBitGen h mbNumBlocks
go :: BitIxGen -> Int -> ST s Bool
go !_ 0 = pure True
go !g !i
| let blockBitIx :: BitIx
(!blockBitIx, !g') = genBitIndex g
= do
assert (let BlockIx b = blockIx
NumBlocks nb = mbNumBlocks
in b >= 0 && b < fromIntegral nb) $ pure ()
b <- BitArray.unsafeRead mbBitArray blockIx blockBitIx
if b then go g' (i + 1)
else pure False
{-# INLINE deserialise #-}
-- | Overwrite the filter's bit array. Use 'new' to create a filter of the
-- expected size and then use this function to fill in the bit data.
--
-- The callback is expected to write (exactly) the given number of bytes into
-- the given byte array buffer.
--
-- See also 'formatVersion' for compatibility advice.
--
deserialise :: PrimMonad m
=> MBloom (PrimState m) a
-> (MutableByteArray (PrimState m) -> Int -> Int -> m ())
-> m ()
deserialise MBloom {mbBitArray} fill =
BitArray.deserialise mbBitArray fill
-------------------------------------------------------------------------------
-- Immutable Bloom filters
--
type Bloom :: Type -> Type
-- | An immutable Bloom filter.
data Bloom a = Bloom {
numBlocks :: {-# UNPACK #-} !NumBlocks -- ^ non-zero
, numHashes :: {-# UNPACK #-} !Int
, hashSalt :: {-# UNPACK #-} !Salt
, bitArray :: {-# UNPACK #-} !BitArray
}
deriving stock Eq
type role Bloom nominal
bloomInvariant :: Bloom a -> Bool
bloomInvariant Bloom {
numBlocks = NumBlocks nb,
numHashes,
bitArray = BitArray.BitArray pa
} =
nb * 8 == sizeofPrimArray pa
&& numHashes > 0
instance Show (Bloom a) where
show mb = "Bloom { " ++ show numBits ++ " bits } "
where
numBits = blocksToBits (numBlocks mb)
instance NFData (Bloom a) where
rnf !_ = ()
-- | Return the size of the Bloom filter.
size :: Bloom a -> BloomSize
size Bloom { numBlocks, numHashes } =
BloomSize {
sizeBits = blocksToBits numBlocks,
sizeHashes = numHashes
}
-- | Query an immutable Bloom filter for membership using already constructed
-- 'Hash' value.
elemHashes :: Bloom a -> Hashes a -> Bool
elemHashes Bloom { numBlocks, numHashes, bitArray } !h =
go g0 numHashes
where
blockIx :: BlockIx
(!blockIx, !g0) = blockIxAndBitGen h numBlocks
go :: BitIxGen -> Int -> Bool
go !_ 0 = True
go !g !i
| let blockBitIx :: BitIx
(!blockBitIx, !g') = genBitIndex g
, assert (let BlockIx b = blockIx
NumBlocks nb = numBlocks
in b >= 0 && b < fromIntegral nb) $
BitArray.unsafeIndex bitArray blockIx blockBitIx
= go g' (i-1)
| otherwise = False
prefetchElem :: Bloom a -> Hashes a -> ST s ()
prefetchElem Bloom { numBlocks, bitArray } !h =
BitArray.prefetchIndex bitArray blockIx
where
blockIx :: BlockIx
(!blockIx, _) = blockIxAndBitGen h numBlocks
-- | Serialise the bloom filter to a 'BloomSize' (which is needed to
-- deserialise) and a 'ByteArray' along with the offset and length containing
-- the filter's bit data.
--
-- See also 'formatVersion' for compatibility advice.
--
serialise :: Bloom a -> (BloomSize, Salt, ByteArray, Int, Int)
serialise b@Bloom{bitArray} =
(size b, hashSalt b, ba, off, len)
where
(ba, off, len) = BitArray.serialise bitArray
-------------------------------------------------------------------------------
-- Conversions between mutable and immutable Bloom filters
--
-- | Create an immutable Bloom filter from a mutable one. The mutable
-- filter may be modified afterwards.
freeze :: MBloom s a -> ST s (Bloom a)
freeze MBloom { mbNumBlocks, mbNumHashes, mbHashSalt, mbBitArray } = do
bitArray <- BitArray.freeze mbBitArray
let !bf = Bloom {
numBlocks = mbNumBlocks,
numHashes = mbNumHashes,
hashSalt = mbHashSalt,
bitArray
}
assert (bloomInvariant bf) $ pure bf
-- | Create an immutable Bloom filter from a mutable one without copying. The
-- mutable filter /must not/ be modified afterwards. For a safer creation
-- interface, use 'freeze' or 'create'.
unsafeFreeze :: MBloom s a -> ST s (Bloom a)
unsafeFreeze MBloom { mbNumBlocks, mbNumHashes, mbHashSalt, mbBitArray } = do
bitArray <- BitArray.unsafeFreeze mbBitArray
let !bf = Bloom {
numBlocks = mbNumBlocks,
numHashes = mbNumHashes,
hashSalt = mbHashSalt,
bitArray
}
assert (bloomInvariant bf) $ pure bf
-- | Copy an immutable Bloom filter to create a mutable one. There is
-- no non-copying equivalent.
thaw :: Bloom a -> ST s (MBloom s a)
thaw Bloom { numBlocks, numHashes, hashSalt, bitArray } = do
mbBitArray <- BitArray.thaw bitArray
pure MBloom {
mbNumBlocks = numBlocks,
mbNumHashes = numHashes,
mbHashSalt = hashSalt,
mbBitArray
}
-------------------------------------------------------------------------------
-- Low level utils
--
{-# INLINE reduceRange32 #-}
-- | Given a word sampled uniformly from the full 'Word32' range, such as a
-- hash, reduce it fairly to a value in the range @[0,n)@.
--
-- See <https://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction/>
--
reduceRange32 :: Word -- ^ Sample from 0..2^32-1
-> Word -- ^ upper bound of range [0,n)
-> Word -- ^ result within range
reduceRange32 x n =
assert (n > 0) $
let w :: Word
w = x * n
in w `shiftR` 32
-------------------------------------------------------------------------------
-- Hashes
--
-- | A small family of hashes, for probing bits in a blocked bloom filter.
--
newtype Hashes a = Hashes Hash
deriving newtype Prim
type role Hashes nominal
{-# INLINE hashesWithSalt #-}
-- | Create a 'Hashes' structure.
hashesWithSalt :: Hashable a => Salt -> a -> Hashes a
hashesWithSalt = \ !salt !x -> Hashes (hashSalt64 salt x)
{-# INLINE blockIxAndBitGen #-}
-- | The scheme for turning 'Hashes' into block and bit indexes is as follows:
-- the high 32bits of the 64bit hash select the block of bits, while the low
-- 32bits are used with a simpler PRNG to produce a sequence of probe points
-- within the selected 512bit block.
--
blockIxAndBitGen :: Hashes a -> NumBlocks -> (BlockIx, BitIxGen)
blockIxAndBitGen (Hashes w64) (NumBlocks numBlocks) =
assert (numBlocks > 0) $
(blockIx, bitGen)
where
blockIx = BlockIx (high32 `reduceRange32` fromIntegral numBlocks)
bitGen = BitIxGen low32
high32, low32 :: Word
high32 = fromIntegral (w64 `shiftR` 32)
low32 = fromIntegral w64 .&. 0xffff_ffff
newtype BitIxGen = BitIxGen Word
{-# INLINE genBitIndex #-}
-- | Generate the next in a short sequence of pseudo-random 9-bit values. This
-- is used for selecting the probe bit within the 512 bit block.
--
-- This simple generator works by multiplying a 32bit value by the golden ratio
-- (as a fraction of a 32bit value). This is only suitable for short sequences
-- using the top few bits each time.
genBitIndex :: BitIxGen -> (BitIx, BitIxGen)
genBitIndex (BitIxGen h) =
(BitIx i, BitIxGen h')
where
i :: Int
i = fromIntegral (h `shiftR` (32-9)) -- top 9 bits
h' :: Word
h' = (h * 0x9e37_79b9) .&. 0xffff_ffff -- keep least significant 32 bits