data-sketches-core-0.2.0.0: src/DataSketches/Quantiles/RelativeErrorQuantile/Internal/Compactor.hs
module DataSketches.Quantiles.RelativeErrorQuantile.Internal.Compactor
( ReqCompactor
, mkReqCompactor
, CompactorReturn (..)
, compact
, getBuffer
, getCoin
, getLgWeight
, getNominalCapacity
, getNumSections
, merge
, nearestEven
) where
import Data.Bits ((.&.), (.|.), complement, countTrailingZeros, shiftL, shiftR)
import Data.Primitive.MutVar
import Data.Word
import DataSketches.Quantiles.RelativeErrorQuantile.Types
import System.Random.MWC (Variate(uniform), Gen)
import Control.Exception (assert)
import Control.Monad (when)
import Control.Monad.Primitive
import DataSketches.Quantiles.RelativeErrorQuantile.Internal.Constants
import DataSketches.Quantiles.RelativeErrorQuantile.Internal.DoubleBuffer
import DataSketches.Core.Internal.URef (MutableFields, newMutableFields, readField, writeField, modifyField)
import DataSketches.Core.Snapshot
data CompactorReturn s = CompactorReturn
{ crDeltaRetItems :: {-# UNPACK #-} !Int
, crDeltaNominalSize :: {-# UNPACK #-} !Int
, crDoubleBuffer :: {-# UNPACK #-} !(DoubleBuffer s)
}
-- | Mutable compactor state packed into a single 'MutableByteArray'.
--
-- All scalar fields are stored at 8-byte-aligned offsets in one contiguous
-- allocation. On x86-64 (64-byte cache lines), the entire 40-byte block
-- fits in a single cache line, so reading state + numSections + sectionSize
-- during compact (the hot path) never causes a second cache miss.
--
-- Layout (byte offset → field):
-- 0: state (Word64) — compaction counter, read/written every compact
-- 8: sectionSizeFlt (Double) — current section size as float
-- 16: sectionSize (Int) — current section size
-- 24: numSections (Int) — current number of sections
-- 32: lastFlip (Int) — last coin flip result (0 or 1)
data ReqCompactor s = ReqCompactor
{ rcRankAccuracy :: !RankAccuracy
, rcLgWeight :: {-# UNPACK #-} !Word8
, rcRng :: {-# UNPACK #-} !(Gen s)
, rcFields :: {-# UNPACK #-} !(MutableFields s)
, rcBuffer :: {-# UNPACK #-} !(MutVar s (DoubleBuffer s))
}
-- Field indices (element index into the Int/Double-sized slots of MutableByteArray)
-- All stored as 8-byte values for uniform alignment.
fState, fSectionSizeFlt, fSectionSize, fNumSections, fLastFlip :: Int
fState = 0 -- Word64, but stored via Int-sized readField/writeField
fSectionSizeFlt = 1 -- Double
fSectionSize = 2 -- Int
fNumSections = 3 -- Int
fLastFlip = 4 -- Int (0 or 1)
fieldBytes :: Int
fieldBytes = 5 * 8 -- 40 bytes total, fits in one 64-byte cache line
-- Typed accessors. These read/write the MutableByteArray at the right element
-- index, relying on Prim instances for Int, Double, Word64.
getStateField :: PrimMonad m => ReqCompactor (PrimState m) -> m Word64
getStateField rc = do
v <- readField (rcFields rc) fState
pure $! fromIntegral (v :: Int)
{-# INLINE getStateField #-}
setStateField :: PrimMonad m => ReqCompactor (PrimState m) -> Word64 -> m ()
setStateField rc v = writeField (rcFields rc) fState (fromIntegral v :: Int)
{-# INLINE setStateField #-}
modifyStateField :: PrimMonad m => ReqCompactor (PrimState m) -> (Word64 -> Word64) -> m ()
modifyStateField rc f = do
!v <- getStateField rc
setStateField rc $! f v
{-# INLINE modifyStateField #-}
getSectionSizeFltField :: PrimMonad m => ReqCompactor (PrimState m) -> m Double
getSectionSizeFltField rc = readField (rcFields rc) fSectionSizeFlt
{-# INLINE getSectionSizeFltField #-}
setSectionSizeFltField :: PrimMonad m => ReqCompactor (PrimState m) -> Double -> m ()
setSectionSizeFltField rc = writeField (rcFields rc) fSectionSizeFlt
{-# INLINE setSectionSizeFltField #-}
getSectionSizeField :: PrimMonad m => ReqCompactor (PrimState m) -> m Int
getSectionSizeField rc = readField (rcFields rc) fSectionSize
{-# INLINE getSectionSizeField #-}
setSectionSizeField :: PrimMonad m => ReqCompactor (PrimState m) -> Int -> m ()
setSectionSizeField rc = writeField (rcFields rc) fSectionSize
{-# INLINE setSectionSizeField #-}
getNumSectionsField :: PrimMonad m => ReqCompactor (PrimState m) -> m Int
getNumSectionsField rc = readField (rcFields rc) fNumSections
{-# INLINE getNumSectionsField #-}
setNumSectionsField :: PrimMonad m => ReqCompactor (PrimState m) -> Int -> m ()
setNumSectionsField rc = writeField (rcFields rc) fNumSections
{-# INLINE setNumSectionsField #-}
getLastFlipField :: PrimMonad m => ReqCompactor (PrimState m) -> m Bool
getLastFlipField rc = (/= (0 :: Int)) <$> readField (rcFields rc) fLastFlip
{-# INLINE getLastFlipField #-}
setLastFlipField :: PrimMonad m => ReqCompactor (PrimState m) -> Bool -> m ()
setLastFlipField rc b = writeField (rcFields rc) fLastFlip (if b then 1 :: Int else 0)
{-# INLINE setLastFlipField #-}
data ReqCompactorSnapshot = ReqCompactorSnapshot
{ snapshotCompactorRankAccuracy :: !RankAccuracy
, snapshotCompactorRankAccuracyState :: !Word64
, snapshotCompactorLastFlip :: !Bool
, snapshotCompactorSectionSizeFlt :: !Double
, snapshotCompactorSectionSize :: !Word32
, snapshotCompactorNumSections :: !Word8
, snapshotCompactorBuffer :: !(Snapshot DoubleBuffer)
} deriving (Show)
instance TakeSnapshot ReqCompactor where
type Snapshot ReqCompactor = ReqCompactorSnapshot
takeSnapshot rc = do
st <- getStateField rc
fl <- getLastFlipField rc
szf <- getSectionSizeFltField rc
sz <- getSectionSizeField rc
ns <- getNumSectionsField rc
buf <- readMutVar (rcBuffer rc) >>= takeSnapshot
pure $ ReqCompactorSnapshot
(rcRankAccuracy rc) st fl szf (fromIntegral sz) (fromIntegral ns) buf
mkReqCompactor
:: PrimMonad m
=> Gen (PrimState m)
-> Word8
-> RankAccuracy
-> Word32
-> m (ReqCompactor (PrimState m))
mkReqCompactor g lgWeight rankAccuracy sectionSize = do
let nominalCapacity = fromIntegral $ nomCapMulti * initNumberOfSections * sectionSize
buff <- mkBuffer (nominalCapacity * 2) nominalCapacity (rankAccuracy == HighRanksAreAccurate)
fields <- newMutableFields fieldBytes
writeField fields fState (0 :: Int)
writeField fields fSectionSizeFlt (fromIntegral sectionSize :: Double)
writeField fields fSectionSize (fromIntegral sectionSize :: Int)
writeField fields fNumSections (fromIntegral initNumberOfSections :: Int)
writeField fields fLastFlip (0 :: Int)
bufMv <- newMutVar buff
pure $ ReqCompactor rankAccuracy lgWeight g fields bufMv
nomCapMult :: Int
nomCapMult = 2
compact :: PrimMonad m => ReqCompactor (PrimState m) -> m (CompactorReturn (PrimState m))
compact this = do
startBuffSize <- getCount =<< getBuffer this
startNominalCapacity <- getNominalCapacity this
numSections <- getNumSectionsField this
sectionSize <- getSectionSizeField this
state <- getStateField this
let trailingOnes = succ $ countTrailingZeros $ complement state
sectionsToCompact = min trailingOnes numSections
(compactionStart, compactionEnd) <- computeCompactionRange this sectionsToCompact
assert (compactionEnd - compactionStart >= 2) $ do
coin <- if state .&. 1 == 1
then fmap not $ getLastFlipField this
else flipCoin this
setLastFlipField this coin
buff <- getBuffer this
promote <- getEvensOrOdds buff compactionStart compactionEnd coin
trimCount buff $ startBuffSize - (compactionEnd - compactionStart)
modifyStateField this (+ 1)
ensureEnoughSections this
endBuffSize <- getCount buff
promoteBuffSize <- getCount promote
endNominalCapacity <- getNominalCapacity this
pure $ CompactorReturn
{ crDeltaRetItems = endBuffSize - startBuffSize + promoteBuffSize
, crDeltaNominalSize = endNominalCapacity - startNominalCapacity
, crDoubleBuffer = promote
}
getLgWeight :: ReqCompactor s -> Word8
getLgWeight = rcLgWeight
getBuffer :: PrimMonad m => ReqCompactor (PrimState m) -> m (DoubleBuffer (PrimState m))
getBuffer = readMutVar . rcBuffer
{-# INLINE getBuffer #-}
flipCoin :: PrimMonad m => ReqCompactor (PrimState m) -> m Bool
flipCoin = uniform . rcRng
{-# INLINE flipCoin #-}
getCoin :: PrimMonad m => ReqCompactor (PrimState m) -> m Bool
getCoin = getLastFlipField
getNominalCapacity :: PrimMonad m => ReqCompactor (PrimState m) -> m Int
getNominalCapacity compactor = do
numSections <- getNumSectionsField compactor
sectionSize <- getSectionSizeField compactor
pure $! nomCapMult * numSections * sectionSize
{-# INLINE getNominalCapacity #-}
getNumSections :: PrimMonad m => ReqCompactor (PrimState m) -> m Word8
getNumSections rc = fromIntegral <$> getNumSectionsField rc
merge
:: (PrimMonad m, s ~ PrimState m)
=> ReqCompactor (PrimState m)
-> ReqCompactor (PrimState m)
-> m (ReqCompactor s)
merge this otherCompactor = assert (rcLgWeight this == rcLgWeight otherCompactor) $ do
otherState <- getStateField otherCompactor
modifyStateField this (.|. otherState)
ensureMaxSections
buff <- getBuffer this
sort buff
otherBuff <- getBuffer otherCompactor
sort otherBuff
otherBuffIsBigger <- (>) <$> getCount otherBuff <*> getCount buff
if otherBuffIsBigger
then do
otherBuff' <- copyBuffer otherBuff
mergeSortIn otherBuff' buff
writeMutVar (rcBuffer this) otherBuff'
else mergeSortIn buff otherBuff
pure this
where
ensureMaxSections = do
adjusted <- ensureEnoughSections this
when adjusted ensureMaxSections
ensureEnoughSections
:: PrimMonad m
=> ReqCompactor (PrimState m)
-> m Bool
ensureEnoughSections compactor = do
sectionSizeFlt <- getSectionSizeFltField compactor
let szf = sectionSizeFlt / sqrt2
ne = nearestEven szf
state <- getStateField compactor
numSections <- getNumSectionsField compactor
sectionSize <- getSectionSizeField compactor
if state >= (1 `shiftL` (numSections - 1))
&& sectionSize > minK
&& ne >= minK
then do
setSectionSizeFltField compactor szf
setSectionSizeField compactor ne
setNumSectionsField compactor (numSections `shiftL` 1)
buf <- getBuffer compactor
nomCapacity <- getNominalCapacity compactor
ensureCapacity buf (2 * nomCapacity)
pure True
else pure False
computeCompactionRange
:: PrimMonad m
=> ReqCompactor (PrimState m)
-> Int
-> m (Int, Int)
computeCompactionRange this secsToCompact = do
buffSize <- getCount =<< getBuffer this
nominalCapacity <- getNominalCapacity this
numSections <- getNumSectionsField this
sectionSize <- getSectionSizeField this
let nonCompact = (nominalCapacity `div` 2) + (numSections - secsToCompact) * sectionSize
nonCompact' = if (buffSize - nonCompact) .&. 1 == 1 then nonCompact - 1 else nonCompact
pure $ case rcRankAccuracy this of
HighRanksAreAccurate -> (0, buffSize - nonCompact')
LowRanksAreAccurate -> (nonCompact', buffSize)
nearestEven :: Double -> Int
nearestEven x = round (x / 2) `shiftL` 1