packages feed

memory-pool-0.1.0.0: src/System/Memory/Pool.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE UnboxedTuples #-}

-- | The goal of this Memory Pool is to provide the ability to allocate big chunks of
-- memory that can fit many `Block`s. Some memory allocators out there have a fairly large
-- minimal size requirement, which would be wasteful if many chunks of small size (eg. 32
-- bytes) are needed at once. Memory pool will allocate one page at a time as more blocks
-- is needed.
--
-- Currently there is no functionality for releasing unused pages. So, once a page is
-- allocated, it will be re-used when more `Block`s is needed, but it will not be GCed
-- until the whole `Pool` is GCed.
module System.Memory.Pool (
  -- * Pool
  Pool,
  initPool,

  -- * Block
  Block (..),
  blockByteCount,
  grabNextBlock,

  -- * Helpers

  -- Exported for testing
  countPages,
  findNextZeroIndex,
) where

import Control.Applicative
import Control.Monad
import Data.Bits
import Data.Primitive.MutVar
import Data.Primitive.PVar
import Data.Primitive.PVar.Unsafe (atomicModifyIntArray#)
import Data.Primitive.PrimArray
import Foreign.ForeignPtr
import Foreign.Ptr
import GHC.Exts (fetchAndIntArray#)
import GHC.ForeignPtr (addForeignPtrConcFinalizer)
import GHC.IO
import GHC.Int
import GHC.ST
import GHC.TypeLits

-- | This is just a proxy type that carries information at the type level about the size
-- of the block in bytes supported by a particular instance of a `Pool`. Use
-- `blockByteCount` to get the byte size at the value level.
data Block (n :: Nat) = Block

-- | Number of bytes in a `Block`
blockByteCount :: KnownNat n => Block n -> Int
blockByteCount = fromIntegral . natVal

-- | Internal helper type that manages each individual page. This is essentially a mutable
-- linked list, which contains a memory buffer, a bit array that tracks which blocks in
-- the buffer are free and which ones are taken.
data Page n s = Page
  { pageMemory :: !(ForeignPtr (Block n))
  -- ^ Contiguous memory buffer that holds all the blocks in the page.
  , pageBitArray :: !(MutablePrimArray s Int)
  -- ^ We use an Int array, because there are no built-in atomic primops for Word.
  , pageFull :: !(PVar Int s)
  -- ^ This is a boolean flag which indicates when a page is full. It here as
  -- optimization only, because it allows us to skip iteration of the above bit
  -- array. It is an `Int` instead of a `Bool`, because GHC provides atomic primops for
  -- ByteArray, whcih is what `PVar` is based on.
  , pageNextPage :: !(MutVar s (Maybe (Page n s)))
  -- ^ Link to the next page. Last page when this IORef contains `Nothing`
  }

-- | Thread-safe lock-free memory pool for managing large memory pages that are made up of
-- many small `Block`s.
data Pool n s = Pool
  { poolFirstPage :: !(Page n s)
  -- ^ Initial page, which itself contains references to subsequent pages
  , poolPageInitializer :: !(ST s (Page n s))
  -- ^ Page initializing action
  , poolBlockFinalizer :: !(Ptr (Block n) -> IO ())
  -- ^ Finilizer that will be attached to each individual `ForeignPtr` of a reserved
  -- `Block`.
  }

-- | Useful function for testing. Check how many pages have been allocated thus far.
countPages :: Pool n s -> ST s Int
countPages pool = go 1 (poolFirstPage pool)
  where
    go n Page {pageNextPage} = do
      readMutVar pageNextPage >>= \case
        Nothing -> pure n
        Just nextPage -> go (n + 1) nextPage

ixBitSize :: Int
ixBitSize = finiteBitSize (0 :: Word)

-- | Initilizes the `Pool` that can be used for further allocation of @`ForeignPtr`
-- `Block` n@ with `grabNextBlock`.
initPool ::
  forall n s.
  KnownNat n =>
  -- | Number of groups per page. Must be a posititve number, otherwise error. One group
  -- contains as many blocks as the operating system has bits. A 64bit architecture will
  -- have 64 blocks per group. For example, if program is compiled on a 64 bit OS and you
  -- know ahead of time the maximum number of blocks that will be allocated through out
  -- the program, then the optimal value for this argument will @maxBlockNum/64@
  Int ->
  -- | Mempool page allocator. Some allocated pages might be immediately discarded,
  -- therefore number of pages utilized will not necessesarely match the number of times
  -- this action will be called.
  (forall a. Int -> ST s (ForeignPtr a)) ->
  -- | Finalizer to use for each block. It is an IO action because it will be executed by
  -- the Garbage Collector in a separate thread once the `Block` is no longer referenced.
  (Ptr (Block n) -> IO ()) ->
  ST s (Pool n s)
initPool groupsPerPage memAlloc blockFinalizer = do
  unless (groupsPerPage > 0) $
    error $
      "Groups per page should be a positive number, but got: "
        ++ show groupsPerPage
  let pageInit = do
        pageMemory <-
          memAlloc $ groupsPerPage * ixBitSize * blockByteCount (Block :: Block n)
        pageBitArray <- newPrimArray groupsPerPage
        setPrimArray pageBitArray 0 groupsPerPage 0
        pageFull <- newPVar 0
        pageNextPage <- newMutVar Nothing
        pure Page {..}
  firstPage <- pageInit
  pure
    Pool
      { poolFirstPage = firstPage
      , poolPageInitializer = pageInit
      , poolBlockFinalizer = blockFinalizer
      }

-- | Reserve a `ForeignPtr` of the `blockByteCount` size in the `Pool`. There is a default
-- finalizer attached to the `ForeignPtr` that will run `Block` pointer finalizer and
-- release that memory for re-use by other blocks allocated in the future. It is safe to
-- add more Haskell finalizers with `addForeignPtrConcFinalizer` if necessary.
grabNextBlock :: KnownNat n => Pool n s -> ST s (ForeignPtr (Block n))
grabNextBlock = grabNextPoolBlockWith grabNextPageForeignPtr
{-# INLINE grabNextBlock #-}

-- | This is a helper function that will allocate a `Page` if the current `Page` in the
-- `Pool` is full. Whenever there are still block slots are available then supplied
-- @grabNext@ function will be used to reserve the slot in that `Page`.
grabNextPoolBlockWith ::
  (Page n s -> (Ptr (Block n) -> IO ()) -> ST s (Maybe (ForeignPtr (Block n)))) ->
  Pool n s ->
  ST s (ForeignPtr (Block n))
grabNextPoolBlockWith grabNext pool = go (poolFirstPage pool)
  where
    go !page = do
      isPageFull <- atomicReadIntPVar (pageFull page)
      if intToBool isPageFull
        then
          readMutVar (pageNextPage page) >>= \case
            Nothing -> do
              newPage <- poolPageInitializer pool
              -- There is a slight chance of a race condition in that the next page could
              -- have been allocated and assigned to 'pageNextPage' by another thread
              -- since we last checked for it. This is not a problem since we can safely
              -- discard the page created in this thread and switch to the one that was
              -- assigned to 'pageNextPage'.
              mNextPage <-
                atomicModifyMutVar' (pageNextPage page) $ \mNextPage ->
                  (mNextPage <|> Just newPage, mNextPage)
              case mNextPage of
                Nothing -> go newPage
                Just existingPage -> do
                  -- Here we cleanup the newly allocated page in favor of the one that
                  -- was potentially created by another thread. It is important to
                  -- eagerly free up scarce resources.
                  --
                  -- This operation is idempotent and thread safe
                  unsafeIOToST $ finalizeForeignPtr (pageMemory newPage)
                  go existingPage
            Just nextPage -> go nextPage
        else
          grabNext page (poolBlockFinalizer pool) >>= \case
            Nothing -> go page
            Just ma -> pure ma
{-# INLINE grabNextPoolBlockWith #-}

intToBool :: Int -> Bool
intToBool 0 = False
intToBool _ = True

-- | This is a helper function that will attempt to find the next available slot for the
-- `Block` and create a `ForeignPtr` with the size of `Block` in the `Page`. In case when
-- `Page` is full it will return `Nothing`.
grabNextPageForeignPtr ::
  forall n s.
  KnownNat n =>
  -- | Page to grab the block from
  Page n s ->
  -- | Finalizer to run, once the `ForeignPtr` holding on to `Ptr` `Block` is no longer used
  (Ptr (Block n) -> IO ()) ->
  ST s (Maybe (ForeignPtr (Block n)))
grabNextPageForeignPtr page finalizer =
  grabNextPageWithAllocator page $ \blockPtr resetIndex -> do
    fp <- newForeignPtr_ blockPtr
    addForeignPtrConcFinalizer fp $ finalizer blockPtr >> resetIndex
    pure fp
{-# INLINE grabNextPageForeignPtr #-}

grabNextPageWithAllocator ::
  forall n s.
  KnownNat n =>
  Page n s ->
  (Ptr (Block n) -> IO () -> IO (ForeignPtr (Block n))) ->
  ST s (Maybe (ForeignPtr (Block n)))
grabNextPageWithAllocator Page {..} allocator = do
  setNextZero pageBitArray >>= \case
    -- There is a slight chance that some Blocks will be cleared before the pageFull is
    -- set to True. This is not a problem because that memory will be recovered as soon as
    -- any other Block in the Page is finalized
    --
    -- TODO: Potentially verify that first Int in pageBitArray has all bits set, in
    -- order to prevent the degenerate case of all Blocks beeing finalized right before
    -- the page is marked as full.
    Nothing -> Nothing <$ atomicWriteIntPVar pageFull 1
    Just ix ->
      fmap Just $
        unsafeIOToST $
          withForeignPtr pageMemory $ \pagePtr ->
            let !blockPtr =
                  plusPtr pagePtr $ ix * blockByteCount (Block :: Block n)
             in allocator blockPtr $ do
                  let !(!q, !r) = ix `quotRem` ixBitSize
                      !pageBitMask = clearBit (complement 0) r
                  touchForeignPtr pageMemory
                  unsafeSTToIO $ atomicAndIntMutablePrimArray pageBitArray q pageBitMask
                  unsafeSTToIO $ atomicWriteIntPVar pageFull 0
{-# INLINE grabNextPageWithAllocator #-}

-- | Atomically AND an element of the array
atomicAndIntMutablePrimArray :: MutablePrimArray s Int -> Int -> Int -> ST s ()
atomicAndIntMutablePrimArray (MutablePrimArray mba#) (I# i#) (I# m#) =
  ST $ \s# ->
    case fetchAndIntArray# mba# i# m# s# of
      (# s'#, _ #) -> (# s'#, () #)
{-# INLINE atomicAndIntMutablePrimArray #-}

-- | Atomically modify an element of the array
atomicModifyMutablePrimArray :: MutablePrimArray s Int -> Int -> (Int -> (Int, a)) -> ST s a
atomicModifyMutablePrimArray (MutablePrimArray mba#) (I# i#) f =
  ST $ atomicModifyIntArray# mba# i# (\x# -> case f (I# x#) of (I# y#, a) -> (# y#, a #))
{-# INLINE atomicModifyMutablePrimArray #-}

-- | Helper function that finds an index of the left-most bit that is not set.
findNextZeroIndex :: forall b. FiniteBits b => b -> Maybe Int
findNextZeroIndex b =
  let !i0 = countTrailingZeros b
      i1 = countTrailingZeros (complement b)
      maxBits = finiteBitSize (undefined :: b)
   in if i0 == 0
        then
          if i1 == maxBits
            then Nothing
            else Just i1
        else Just (i0 - 1)
{-# INLINE findNextZeroIndex #-}

-- | Finds an index of the next bit that is not set in the bit array and flips it
-- atomically. In case when all bits are set, then `Nothing` is returned. It is possible
-- that while search is ongoing bits that where checked get cleared. This is totally fine
-- for our implementation of mempool.
setNextZero :: MutablePrimArray s Int -> ST s (Maybe Int)
setNextZero ma = ifindAtomicMutablePrimArray ma f
  where
    f i !w =
      case findNextZeroIndex w of
        Nothing -> (w, Nothing)
        Just !bitIx -> (setBit w bitIx, Just (ixBitSize * i + bitIx))
{-# INLINE setNextZero #-}

ifindAtomicMutablePrimArray ::
  MutablePrimArray s Int ->
  (Int -> Int -> (Int, Maybe a)) ->
  ST s (Maybe a)
ifindAtomicMutablePrimArray ma f = do
  n <- getSizeofMutablePrimArray ma
  let go i
        | i >= n = pure Nothing
        | otherwise =
            atomicModifyMutablePrimArray ma i (f i) >>= \case
              Nothing -> go (i + 1)
              Just a -> pure $ Just a
  go 0
{-# INLINE ifindAtomicMutablePrimArray #-}