http2-1.4.3: bench-priority/RingOfQueues.hs
{-# LANGUAGE ForeignFunctionInterface #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE FlexibleContexts #-}
-- Haskell implementation of H2O's priority queue.
-- https://github.com/h2o/h2o/blob/master/lib/http2/scheduler.c
module RingOfQueues (
Entry
, newEntry
, renewEntry
, item
, Node
, PriorityQueue(..)
, new
, enqueue
, dequeue
, delete
) where
import Control.Monad (replicateM)
import Data.Array (Array, listArray, (!))
import Data.Bits (setBit, clearBit, shiftR)
import Data.IORef
import Data.Word (Word64)
import Foreign.C.Types (CLLong(..))
import DoublyLinkedQueueIO (Queue, Node)
import qualified DoublyLinkedQueueIO as Q
----------------------------------------------------------------
type Weight = Int
-- | Abstract data type of entries for priority queues.
data Entry a = Entry {
item :: a -- ^ Extracting an item from an entry.
, weight :: {-# UNPACK #-} !Weight
, deficit :: {-# UNPACK #-} !Int
} deriving Show
newEntry :: a -> Weight -> Entry a
newEntry x w = Entry x w 0
-- | Changing the item of an entry.
renewEntry :: Entry a -> b -> Entry b
renewEntry ent x = ent { item = x }
----------------------------------------------------------------
data PriorityQueue a = PriorityQueue {
bitsRef :: IORef Word64
, offsetRef :: IORef Int
, queues :: Array Int (Queue (Entry a))
}
----------------------------------------------------------------
bitWidth :: Int
bitWidth = 64
relativeIndex :: Int -> Int -> Int
relativeIndex idx offset = (offset + idx) `mod` bitWidth
----------------------------------------------------------------
deficitSteps :: Int
deficitSteps = 65536
deficitList :: [Int]
deficitList = map calc idxs
where
idxs :: [Double]
idxs = [1..256]
calc w = round (65536 * 63 / w)
deficitTable :: Array Int Int
deficitTable = listArray (1,256) deficitList
----------------------------------------------------------------
-- https://en.wikipedia.org/wiki/Find_first_set
foreign import ccall unsafe "strings.h ffsll"
c_ffs :: CLLong -> CLLong
-- | Finding first bit set. O(1)
--
-- >>> firstBitSet $ setBit 0 63
-- 63
-- >>> firstBitSet $ setBit 0 62
-- 62
-- >>> firstBitSet $ setBit 0 1
-- 1
-- >>> firstBitSet $ setBit 0 0
-- 0
-- >>> firstBitSet 0
-- -1
firstBitSet :: Word64 -> Int
firstBitSet x = ffs x - 1
where
ffs = fromIntegral . c_ffs . fromIntegral
----------------------------------------------------------------
new :: IO (PriorityQueue a)
new = PriorityQueue <$> newIORef 0 <*> newIORef 0 <*> newQueues
where
newQueues = listArray (0, bitWidth - 1) <$> replicateM bitWidth Q.new
-- | Enqueuing an entry. PriorityQueue is updated.
enqueue :: Entry a -> PriorityQueue a -> IO (Node (Entry a))
enqueue ent PriorityQueue{..} = do
let (!idx,!deficit') = calcIdxAndDeficit
!offidx <- getOffIdx idx
node <- push offidx ent { deficit = deficit' }
updateBits idx
return node
where
calcIdxAndDeficit = total `divMod` deficitSteps
where
total = deficitTable ! weight ent + deficit ent
getOffIdx idx = relativeIndex idx <$> readIORef offsetRef
push offidx ent' = Q.enqueue ent' (queues ! offidx)
updateBits idx = modifyIORef' bitsRef $ flip setBit idx
-- | Dequeuing an entry. PriorityQueue is updated.
dequeue :: PriorityQueue a -> IO (Maybe (Entry a))
dequeue pq@PriorityQueue{..} = do
!idx <- getIdx
if idx == -1 then
return Nothing
else do
!offidx <- getOffIdx idx
updateOffset offidx
queueIsEmpty <- checkEmpty offidx
updateBits idx queueIsEmpty
if queueIsEmpty then
dequeue pq
else
Just <$> pop offidx
where
getIdx = firstBitSet <$> readIORef bitsRef
getOffIdx idx = relativeIndex idx <$> readIORef offsetRef
pop offidx = Q.dequeue (queues ! offidx)
checkEmpty offidx = Q.isEmpty (queues ! offidx)
updateOffset offset' = writeIORef offsetRef offset'
updateBits idx isEmpty = modifyIORef' bitsRef shiftClear
where
shiftClear bits
| isEmpty = clearBit (shiftR bits idx) 0
| otherwise = shiftR bits idx
-- bits is not updated because it's difficult.
delete :: Node (Entry a) -> IO ()
delete node = Q.delete node