{-# LANGUAGE CPP, BangPatterns, Rank2Types #-}
module FastPut where
import Foreign
import Data.Monoid
import Control.Monad (unless)
import qualified Data.ByteString as S
import qualified Data.ByteString.Lazy as L
#ifdef BYTESTRING_IN_BASE
import Data.ByteString.Base (inlinePerformIO)
import qualified Data.ByteString.Base as S
import qualified Data.ByteString.Lazy.Base as L
#else
import Data.ByteString.Internal (inlinePerformIO)
import qualified Data.ByteString.Internal as S
import qualified Data.ByteString.Lazy.Internal as L
#endif
import qualified Blaze.ByteString.Builder.Internal as B
import qualified Blaze.ByteString.Builder.Write as B
import Blaze.ByteString.Builder.Write (Write(..))
import qualified Blaze.ByteString.Builder.Word as B
import Blaze.ByteString.Builder.Word (writeWord8)
import Criterion.Main
main :: IO ()
main = defaultMain $ concat
[ return $ bench "cost of putBuilder" $ whnf
(L.length . toLazyByteString2 . mapM_ (fromBuilder . fromWord8))
word8s
, benchmark "putBuilder"
(fromBuilder . mconcat . map fromWord8)
(mconcat . map B.fromWord8)
word8s
, benchmark "fromWriteSingleton"
(mapM_ putWord8)
(mconcat . map B.fromWord8)
word8s
, benchmark "fromWrite"
(mapM_ (putWrite . writeWord8))
(mconcat . map (B.fromWrite . writeWord8))
word8s
]
where
benchmark name putF builderF x =
[ bench (name ++ " Put") $
whnf (L.length . toLazyByteString2 . putF) x
, bench (name ++ " Builder") $
whnf (L.length . B.toLazyByteString . builderF) x
]
word8s :: [Word8]
word8s = take 100000 $ cycle [0..]
{-# NOINLINE word8s #-}
data BufRange = BufRange {-# UNPACK #-} !(Ptr Word8) {-# UNPACK #-} !(Ptr Word8)
newtype Put a = Put {
unPut :: forall r. (a -> PutStep r) -> PutStep r
}
data PutSignal a =
Done {-# UNPACK #-} !(Ptr Word8) a
| BufferFull
{-# UNPACK #-} !Int
{-# UNPACK #-} !(Ptr Word8)
!(PutStep a)
| InsertByteString
{-# UNPACK #-} !(Ptr Word8)
!S.ByteString
!(PutStep a)
type PutStep a = BufRange -> IO (PutSignal a)
instance Monad Put where
return x = Put $ \k -> k x
{-# INLINE return #-}
m >>= f = Put $ \k -> unPut m (\x -> unPut (f x) k)
{-# INLINE (>>=) #-}
m >> n = Put $ \k -> unPut m (\_ -> unPut n k)
{-# INLINE (>>) #-}
newtype Builder = Builder (forall r. PutStep r -> PutStep r)
instance Monoid Builder where
mempty = Builder id
{-# INLINE mempty #-}
(Builder b1) `mappend` (Builder b2) = Builder $ b1 . b2
{-# INLINE mappend #-}
mconcat = foldr mappend mempty
{-# INLINE mconcat #-}
fromBuilder :: Builder -> Put ()
fromBuilder (Builder build) = Put $ \k -> build (k ())
toBuilder :: Put () -> Builder
toBuilder (Put put) = Builder $ \k -> put (\_ -> k)
fromWrite :: Write -> Builder
fromWrite (Write size io) =
Builder step
where
step k (BufRange pf pe)
| pf `plusPtr` size <= pe = do
io pf
let !br' = BufRange (pf `plusPtr` size) pe
k br'
| otherwise = return $ BufferFull size pf (step k)
{-# INLINE fromWrite #-}
fromWriteSingleton :: (a -> Write) -> a -> Builder
fromWriteSingleton write =
mkPut
where
mkPut x = Builder step
where
step k (BufRange pf pe)
| pf `plusPtr` size <= pe = do
io pf
let !br' = BufRange (pf `plusPtr` size) pe
k br'
| otherwise = return $ BufferFull size pf (step k)
where
Write size io = write x
{-# INLINE fromWriteSingleton #-}
fromWord8 :: Word8 -> Builder
fromWord8 = fromWriteSingleton writeWord8
putWord8 :: Word8 -> Put ()
putWord8 = putWriteSingleton writeWord8
putWrite :: Write -> Put ()
putWrite (Write size io) =
Put step
where
step k (BufRange pf pe)
| pf `plusPtr` size <= pe = do
io pf
let !br' = BufRange (pf `plusPtr` size) pe
k () br'
| otherwise = return $ BufferFull size pf (step k)
{-# INLINE putWrite #-}
putWriteSingleton :: (a -> Write) -> a -> Put ()
putWriteSingleton write =
mkPut
where
mkPut x = Put step
where
step k (BufRange pf pe)
| pf `plusPtr` size <= pe = do
io pf
let !br' = BufRange (pf `plusPtr` size) pe
k () br'
| otherwise = return $ BufferFull size pf (step k)
where
Write size io = write x
{-# INLINE putWriteSingleton #-}
putBuilder :: B.Builder -> Put ()
putBuilder (B.Builder b) =
Put step
where
finalStep _ pf = return $ B.Done pf
step k = go (b finalStep)
where
go buildStep (BufRange pf pe) = do
signal <- buildStep pf pe
case signal of
B.Done pf' -> do
let !br' = BufRange pf' pe
k () br'
B.BufferFull minSize pf' nextBuildStep ->
return $ BufferFull minSize pf' (go nextBuildStep)
B.ModifyChunks _ _ _ ->
error "putBuilder: ModifyChunks not implemented"
defaultBufferSize :: Int
defaultBufferSize = 32 * 1024 - overhead
where overhead = 2 * sizeOf (undefined :: Int)
defaultMinimalBufferSize :: Int
defaultMinimalBufferSize = 4 * 1024 - overhead
where overhead = 2 * sizeOf (undefined :: Int)
defaultFirstBufferSize :: Int
defaultFirstBufferSize = 64
defaultMaximalCopySize :: Int
defaultMaximalCopySize = 2 * defaultMinimalBufferSize
toLazyByteStringWith
:: Int
-> Int
-> Int
-> Put a
-> L.ByteString
-> L.ByteString
toLazyByteStringWith bufSize minBufSize firstBufSize (Put b) k =
inlinePerformIO $ fillFirstBuffer (b finalStep)
where
finalStep _ (BufRange pf _) = return $ Done pf undefined
fillFirstBuffer !step0
| minBufSize <= firstBufSize = fillNewBuffer firstBufSize step0
| otherwise = do
fpbuf <- S.mallocByteString firstBufSize
withForeignPtr fpbuf $ \pf -> do
let !br = BufRange pf (pf `plusPtr` firstBufSize)
mkbs pf' = S.PS fpbuf 0 (pf' `minusPtr` pf)
{-# INLINE mkbs #-}
next <- step0 br
case next of
Done pf' _
| pf' == pf -> return k
| otherwise -> return $ L.Chunk (mkbs pf') k
BufferFull newSize pf' nextStep -> do
let !l = pf' `minusPtr` pf
fillNewBuffer (max (l + newSize) minBufSize) $
\(BufRange pfNew peNew) -> do
copyBytes pfNew pf l
let !brNew = BufRange (pfNew `plusPtr` l) peNew
nextStep brNew
InsertByteString _ _ _ -> error "not yet implemented"
fillNewBuffer !size !step0 = do
fpbuf <- S.mallocByteString size
withForeignPtr fpbuf $ fillBuffer fpbuf
where
fillBuffer fpbuf !pbuf = fill pbuf step0
where
!pe = pbuf `plusPtr` size
fill !pf !step = do
let !br = BufRange pf pe
next <- step br
let mkbs pf' = S.PS fpbuf (pf `minusPtr` pbuf) (pf' `minusPtr` pf)
{-# INLINE mkbs #-}
case next of
Done pf' _
| pf' == pf -> return k
| otherwise -> return $ L.Chunk (mkbs pf') k
BufferFull newSize pf' nextStep ->
return $ L.Chunk (mkbs pf')
(inlinePerformIO $
fillNewBuffer (max newSize bufSize) nextStep)
InsertByteString _ _ _ -> error "not yet implemented2"
toLazyByteString :: Put a -> L.ByteString
toLazyByteString b = toLazyByteStringWith
defaultBufferSize defaultMinimalBufferSize defaultFirstBufferSize b L.empty
{-# INLINE toLazyByteString #-}
data BuildStream a =
BuildChunk S.ByteString (IO (BuildStream a))
| BuildYield
a
(forall b. Bool ->
Either (Maybe S.ByteString) (Put b -> IO (BuildStream b)))
enumPut :: Int -> Put a -> IO (BuildStream a)
enumPut bufSize (Put put0) =
fillBuffer bufSize (put0 finalStep)
where
finalStep :: forall b. b -> PutStep b
finalStep x (BufRange op _) = return $ Done op x
fillBuffer :: forall b. Int -> PutStep b -> IO (BuildStream b)
fillBuffer size step = do
fpbuf <- S.mallocByteString bufSize
let !pbuf = unsafeForeignPtrToPtr fpbuf
!br = BufRange pbuf (pbuf `plusPtr` size)
fillStep fpbuf br step
fillPut :: ForeignPtr Word8 -> BufRange ->
Bool -> Either (Maybe S.ByteString) (Put b -> IO (BuildStream b))
fillPut !fpbuf !(BufRange op _) False
| pbuf == op = Left Nothing
| otherwise = Left $ Just $
S.PS fpbuf 0 (op `minusPtr` pbuf)
where
pbuf = unsafeForeignPtrToPtr fpbuf
{-# INLINE pbuf #-}
fillPut !fpbuf !br True =
Right $ \(Put put) -> fillStep fpbuf br (put finalStep)
fillStep :: forall b. ForeignPtr Word8 -> BufRange -> PutStep b -> IO (BuildStream b)
fillStep !fpbuf !br@(BufRange _ ope) step = do
let pbuf = unsafeForeignPtrToPtr fpbuf
{-# INLINE pbuf #-}
signal <- step br
case signal of
Done op' x -> do
let !br' = BufRange op' ope
return $ BuildYield x (fillPut fpbuf br')
BufferFull minSize op' nextStep
| pbuf == op' -> do
fillBuffer (max bufSize minSize) nextStep
| otherwise -> do
return $ BuildChunk
(S.PS fpbuf 0 (op' `minusPtr` pbuf))
(fillBuffer (max bufSize minSize) nextStep)
InsertByteString op' bs nextStep
| S.null bs -> do
let !br' = BufRange op' ope
fillStep fpbuf br' nextStep
| pbuf == op' -> do
return $ BuildChunk bs (fillBuffer bufSize nextStep)
| otherwise -> do
return $ BuildChunk (S.PS fpbuf 0 (op' `minusPtr` pbuf))
(return $ BuildChunk bs (fillBuffer bufSize nextStep))
toLazyByteString' :: Put () -> L.ByteString
toLazyByteString' put =
inlinePerformIO (consume `fmap` enumPut defaultBufferSize put)
where
consume :: BuildStream () -> L.ByteString
consume (BuildYield _ f) =
case f False of
Left Nothing -> L.Empty
Left (Just bs) -> L.Chunk bs L.Empty
Right _ -> error "toLazyByteString': enumPut violated postcondition"
consume (BuildChunk bs ioStream) =
L.Chunk bs $ inlinePerformIO (consume `fmap` ioStream)
data Buffer = Buffer {-# UNPACK #-} !(ForeignPtr Word8)
{-# UNPACK #-} !(Ptr Word8)
{-# UNPACK #-} !(Ptr Word8)
{-# UNPACK #-} !(Ptr Word8)
allocBuffer :: Int -> IO Buffer
allocBuffer size = do
fpbuf <- S.mallocByteString size
let !pbuf = unsafeForeignPtrToPtr fpbuf
return $! Buffer fpbuf pbuf pbuf (pbuf `plusPtr` size)
unsafeFreezeBuffer :: Buffer -> S.ByteString
unsafeFreezeBuffer (Buffer fpbuf p0 op _) =
S.PS fpbuf 0 (op `minusPtr` p0)
unsafeFreezeNonEmptyBuffer :: Buffer -> Maybe S.ByteString
unsafeFreezeNonEmptyBuffer (Buffer fpbuf p0 op _)
| p0 == op = Nothing
| otherwise = Just $ S.PS fpbuf 0 (op `minusPtr` p0)
nextSlice :: Int -> Buffer -> Maybe Buffer
nextSlice minSize (Buffer fpbuf _ op ope)
| ope `minusPtr` op <= minSize = Nothing
| otherwise = Just (Buffer fpbuf op op ope)
runPut :: Monad m
=> (IO (PutSignal a) -> m (PutSignal a))
-> (Int -> Buffer -> m Buffer)
-> (S.ByteString -> m ())
-> Put a
-> Buffer
-> m (a, Buffer)
runPut liftIO outputBuf outputBS (Put put) =
runStep (put finalStep)
where
finalStep x !(BufRange op _) = return $ Done op x
runStep step buf@(Buffer fpbuf p0 op ope) = do
let !br = BufRange op ope
signal <- liftIO $ step br
case signal of
Done op' x ->
return (x, Buffer fpbuf p0 op' ope)
BufferFull minSize op' nextStep -> do
buf' <- outputBuf minSize (Buffer fpbuf p0 op' ope)
runStep nextStep buf'
InsertByteString op' bs nextStep
| S.null bs ->
outputBuf 1 (Buffer fpbuf p0 op' ope) >>= runStep nextStep
| p0 == op' -> do
outputBS bs
runStep nextStep buf
| otherwise -> do
buf' <- outputBuf 1 (Buffer fpbuf p0 op' ope)
outputBS bs
runStep nextStep buf'
{-# INLINE runPut #-}
newtype LBSM a = LBSM { unLBSM :: (a, L.ByteString -> L.ByteString) }
instance Monad LBSM where
return x = LBSM (x, id)
(LBSM (x,k)) >>= f = let LBSM (x',k') = f x in LBSM (x', k . k')
(LBSM (_,k)) >> (LBSM (x',k')) = LBSM (x', k . k')
toLazyByteString2 :: Put a -> L.ByteString
toLazyByteString2 put =
k (bufToLBSCont (snd result) L.empty)
where
buf0 = inlinePerformIO $ allocBuffer defaultBufferSize
LBSM (result, k) = runPut liftIO outputBuf outputBS put buf0
bufToLBSCont = maybe id L.Chunk . unsafeFreezeNonEmptyBuffer
liftIO io = LBSM (inlinePerformIO io, id)
outputBuf minSize buf = LBSM
( inlinePerformIO $ allocBuffer (max minSize defaultBufferSize)
, bufToLBSCont buf )
outputBS bs = LBSM ((), L.Chunk bs)
traceBuilder :: String -> Builder
traceBuilder msg = Builder $ \k br@(BufRange op ope) -> do
putStrLn $ "traceBuilder " ++ show (op, ope) ++ ": " ++ msg
k br
flushBuilder :: Builder
flushBuilder = Builder $ \k (BufRange op _) -> do
return $ InsertByteString op S.empty k
test2 :: Word8 -> [S.ByteString]
test2 x = L.toChunks $ toLazyByteString2 $ fromBuilder $ mconcat
[ traceBuilder "before flush"
, fromWord8 48
, flushBuilder
, flushBuilder
, traceBuilder "after flush"
, fromWord8 x
]