on master help sign in
implement arraysOf in terms of chunksOf

* Reimplement groupsOf for better fusion * Use A.writeNUnsafe for better fusion with groupsOf Now we can use chunksOf/groupsOf to implement arraysOf with equivalent performance. This commit hides the arraysOf from the Array module and export it via Streamly.Internal because in one case (writeS) it still is faster. This can be resolved once GHC issue is resolved. Even though writeS is not an officially exported API, this case indicates that there may potentially be more such cases which do not fuse well, so we are keeping this as a backup for such cases for now.

commit9c419e62f2a8f0e1fa08125ec14184c9279ff74a authorHarendra Kumar <harendra.kumar@gmail.com> dateMon 19 Aug 2019, 13:24 (7 years ago) parent120175c
12 files changed+122−71
src/Streamly/FileSystem/FD.hs+2−1 src/Streamly/FileSystem/File.hs+3−2 src/Streamly/FileSystem/Handle.hs+2−2 src/Streamly/FileSystem/Handle/Internal.hs+12−1 src/Streamly/Internal.hs+2 src/Streamly/Memory/Array.hs+2−2 src/Streamly/Memory/Array/Types.hs+45−3 src/Streamly/Network/Client.hs+3−2 src/Streamly/Network/Socket.hs+3−2 src/Streamly/Prelude/Internal.hs+15−1 src/Streamly/Streams/StreamD/Type.hs+30−53 test/Arrays.hs+3−2
src/Streamly/FileSystem/FD.hs+2−1
@@ -150,6 +150,7 @@ import qualified Streamly.FileSystem.FDIO as RawIO hiding (write)
150150-- import Streamly.String (encodeUtf8, decodeUtf8, foldLines)
151151
152152import qualified Streamly.Memory.Array as A
153+import qualified Streamly.Memory.ArrayStream as AS
153154import qualified Streamly.Prelude as S
154155import qualified Streamly.Streams.StreamD.Type as D
155156
@@ -399,7 +400,7 @@ _writevArraysPackedUpto n h xs =
399400-- @since 0.7.0
400401{-# INLINE writeInChunksOf #-}
401402writeInChunksOf :: MonadIO m => Int -> Handle -> SerialT m Word8 -> m ()
402−writeInChunksOf n h m = writeArrays h $ A.arraysOf n m
403+writeInChunksOf n h m = writeArrays h $ AS.arraysOf n m
403404
404405-- > write = 'writeInChunksOf' A.defaultChunkSize
405406--
src/Streamly/FileSystem/File.hs+3−2
@@ -124,6 +124,7 @@ import Streamly.SVar (MonadAsync)
124124
125125import qualified Streamly.FileSystem.Handle as FH
126126import qualified Streamly.Memory.Array as A
127+import qualified Streamly.Memory.ArrayStream as AS
127128import qualified Streamly.Prelude as S
128129
129130-------------------------------------------------------------------------------
@@ -289,7 +290,7 @@ writeArrays = writeArraysMode WriteMode
289290{-# INLINE writeInChunksOf #-}
290291writeInChunksOf :: (MonadAsync m, MonadCatch m)
291292 => Int -> FilePath -> SerialT m Word8 -> m ()
292−writeInChunksOf n file xs = writeArrays file $ A.arraysOf n xs
293+writeInChunksOf n file xs = writeArrays file $ AS.arraysOf n xs
293294
294295-- > write = 'writeInChunksOf' defaultChunkSize
295296--
@@ -325,7 +326,7 @@ appendArrays = writeArraysMode AppendMode
325326{-# INLINE appendByChunks #-}
326327appendByChunks :: (MonadAsync m, MonadCatch m)
327328 => Int -> FilePath -> SerialT m Word8 -> m ()
328−appendByChunks n file xs = appendArrays file $ A.arraysOf n xs
329+appendByChunks n file xs = appendArrays file $ AS.arraysOf n xs
329330
330331-- | Append a byte stream to a file. Combines the bytes in chunks of size up to
331332-- 'A.defaultChunkSize' before writing. If the file exists then the new data
src/Streamly/FileSystem/Handle.hs+2−2
@@ -119,7 +119,7 @@ import System.IO (Handle, hGetBufSome)
119119import Prelude hiding (read)
120120
121121import Streamly.FileSystem.Handle.Internal (writeArray)
122−import Streamly.Memory.Array.Types (Array(..))
122+import Streamly.Memory.Array.Types (Array(..), writeNUnsafe)
123123import Streamly.Streams.StreamK.Type (IsStream, mkStream)
124124import Streamly.Memory.Array.Types
125125 (defaultChunkSize, shrinkToFit, lpackArraysChunksOf)
@@ -283,7 +283,7 @@ writeArraysInChunksOf n h = lpackArraysChunksOf n (writeArrays h)
283283-- @since 0.7.0
284284{-# INLINE writeInChunksOf #-}
285285writeInChunksOf :: MonadIO m => Int -> Handle -> Fold m Word8 ()
286−writeInChunksOf n h = FL.lchunksOf n (A.writeN n) (writeArrays h)
286+writeInChunksOf n h = FL.lchunksOf n (writeNUnsafe n) (writeArrays h)
287287
288288-- > write = 'writeInChunksOf' A.defaultChunkSize
289289--
src/Streamly/FileSystem/Handle/Internal.hs+12−1
@@ -46,7 +46,7 @@ import Streamly.Memory.Array.Types (defaultChunkSize)
4646
4747import qualified Streamly.Memory.Array as A
4848import qualified Streamly.Memory.ArrayStream as AS
49−import qualified Streamly.Prelude as S
49+import qualified Streamly.Prelude.Internal as S
5050
5151-------------------------------------------------------------------------------
5252-- Array IO (output)
@@ -91,6 +91,16 @@ writeSArraysInChunksOf :: (MonadIO m, Storable a)
9191 => Int -> Handle -> SerialT m (Array a) -> m ()
9292writeSArraysInChunksOf n h xs = writeSArrays h $ AS.compact n xs
9393
94+-- XXX Using custom array chunking with A.arraysOf, writeS gives 2x faster
95+-- readStream/catStream benchmark compared to S.arraysOf based writeS as well
96+-- as compared to the fold IO version of cat benchmark i.e. readStream/cat.
97+-- However, if we use the most optimal A.writeNUnsafe fold then the fold
98+-- version of cat is also as fast. But with the most optimal version of
99+-- A.writeNUnsafe, fusion breaks. Once GHC fusion problem gets fixed we should
100+-- be able to use that.
101+--
102+-- Until then we are keeping the custom A.arraysOf code.
103+--
94104-- | @writeSInChunksOf chunkSize handle stream@ writes @stream@ to @handle@ in
95105-- chunks of @chunkSize@. A write is performed to the IO device as soon as we
96106-- collect the required input size.
@@ -98,6 +108,7 @@ writeSArraysInChunksOf n h xs = writeSArrays h $ AS.compact n xs
98108-- @since 0.7.0
99109{-# INLINE writeSInChunksOf #-}
100110writeSInChunksOf :: MonadIO m => Int -> Handle -> SerialT m Word8 -> m ()
111+-- writeSInChunksOf n h m = writeSArrays h $ S.arraysOf n m
101112writeSInChunksOf n h m = writeSArrays h $ AS.arraysOf n m
102113
103114-- > write = 'writeInChunksOf' A.defaultChunkSize
src/Streamly/Internal.hs+2
@@ -74,6 +74,7 @@ module Streamly.Internal
7474
7575 -- * Streamly.Memory.Array
7676 , readU
77+ , A.arraysOf
7778
7879 -- * Streamly.FileSystem.Handle
7980 , writeS
@@ -88,5 +89,6 @@ import Streamly.Streams.Combinators (inspectMode)
8889import Streamly.Streams.Parallel (tapAsync)
8990import Streamly.Unfold.Types (Unfold(..))
9091
92+import qualified Streamly.Memory.ArrayStream as A
9193import Streamly.Prelude.Internal
9294import Streamly.Fold.Types
src/Streamly/Memory/Array.hs+2−2
@@ -121,8 +121,8 @@ module Streamly.Memory.Array
121121 -}
122122
123123 -- * Streams of Arrays
124− -- ** Creation
125− , AS.arraysOf
124+ -- -- ** Creation
125+ -- , AS.arraysOf
126126
127127 -- ** Elimination
128128 , AS.concat
src/Streamly/Memory/Array/Types.hs+45−3
@@ -61,6 +61,7 @@ module Streamly.Memory.Array.Types
6161 , toStreamKRev
6262 , toList
6363 , writeN
64+ , writeNUnsafe
6465 , read
6566 , readU
6667
@@ -511,15 +512,56 @@ writeN n = Fold step initial extract
511512
512513 where
513514
514− initial = do
515− if n < 0 then error "writeN: negative count specified" else return ()
516− liftIO $ newArray n
515+ initial = liftIO $ newArray (max n 0)
517516 step arr@(Array _ end bound) _ | end == bound = return arr
518517 step (Array start end bound) x = do
519518 liftIO $ poke end x
520519 return $ Array start (end `plusPtr` sizeOf (undefined :: a)) bound
521520 extract = return -- liftIO . shrinkToFit
522521
522+-- | Like 'writeN' but does not check the array bounds when writing. The fold
523+-- driver must not call the step function more than 'n' times otherwise it will
524+-- corrupt the memory and crash. This function exists mainly because any
525+-- conditional in the step function blocks fusion causing 10x performance
526+-- slowdown.
527+--
528+-- @since 0.7.0
529+{-# INLINE_NORMAL writeNUnsafe #-}
530+writeNUnsafe :: forall m a. (MonadIO m, Storable a)
531+ => Int -> Fold m a (Array a)
532+writeNUnsafe n = Fold step initial extract
533+
534+ where
535+
536+ {-
537+ -- XXX this implementation is good on all benchmarks except
538+ -- readStream/catStream which uses a stream based writeS instead of a fold
539+ -- based write. In the writeS case it does not fuse and therefore performs
540+ -- 10x worse.
541+ initial = do
542+ (Array start end _) <- liftIO $ newArray (max n 0)
543+ return (start, end)
544+ -- XXX any if condition in the step causes fusion to fail with groupsOf
545+ -- If we need to do bounds check we can use 'n' instead of threading around
546+ -- bound pointer.
547+ step (start, end) x = do
548+ liftIO $ poke end x
549+ return $ (start, (end `plusPtr` sizeOf (undefined :: a)))
550+ extract (start, end) = return $ Array start end end -- liftIO . shrinkToFit
551+ -}
552+ -- This implementation fuses in all known cases as of now. Even though both
553+ -- readStream/cat and readStream/catStream benchmarks perform 2x worse when
554+ -- arraysOf uses this impl compared to their best seen performance.
555+ -- readStream/cat is best with the above implementation and
556+ -- readStream/catStream is best when using the custom arraysOf
557+ -- implementation.
558+ initial = do
559+ liftIO $ newArray (max n 0)
560+ step (Array start end bound) x = do
561+ liftIO $ poke end x
562+ return $ Array start (end `plusPtr` sizeOf (undefined :: a)) bound
563+ extract = return -- liftIO . shrinkToFit
564+
523565{-# INLINE_NORMAL fromStreamDN #-}
524566fromStreamDN :: forall m a. (MonadIO m, Storable a)
525567 => Int -> D.Stream m a -> m (Array a)
src/Streamly/Network/Client.hs+3−2
@@ -67,7 +67,7 @@ import qualified Network.Socket as Net
6767
6868import Streamly (MonadAsync)
6969import Streamly.Fold.Types (Fold(..))
70−import Streamly.Memory.Array.Types (Array(..), defaultChunkSize)
70+import Streamly.Memory.Array.Types (Array(..), defaultChunkSize, writeNUnsafe)
7171-- import Streamly.Streams.Serial (SerialT)
7272import Streamly.Streams.StreamK.Type (IsStream)
7373
@@ -179,7 +179,8 @@ writeInChunksOf
179179 -> (Word8, Word8, Word8, Word8)
180180 -> PortNumber
181181 -> Fold m Word8 ()
182−writeInChunksOf n addr port = FL.lchunksOf n (A.writeN n) (writeArrays addr port)
182+writeInChunksOf n addr port =
183+ FL.lchunksOf n (writeNUnsafe n) (writeArrays addr port)
183184
184185{-
185186-- | Write a stream to the supplied IPv4 host address and port number.
src/Streamly/Network/Socket.hs+3−2
@@ -95,6 +95,7 @@ import Streamly.Fold (Fold)
9595import qualified Streamly.Fold as FL
9696import qualified Streamly.Fold.Types as FL
9797import qualified Streamly.Memory.Array as A
98+import qualified Streamly.Memory.ArrayStream as AS
9899import qualified Streamly.Memory.Array.Types as A
99100import qualified Streamly.Prelude as S
100101
@@ -301,7 +302,7 @@ writeArrays h = FL.drainBy (liftIO . writeArray h)
301302-- @since 0.7.0
302303{-# INLINE writeInChunksOfS #-}
303304writeInChunksOfS :: MonadIO m => Int -> Socket -> SerialT m Word8 -> m ()
304−writeInChunksOfS n h m = writeArraysS h $ A.arraysOf n m
305+writeInChunksOfS n h m = writeArraysS h $ AS.arraysOf n m
305306
306307-- | Write a byte stream to a socket. Accumulates the input in chunks of
307308-- specified number of bytes before writing.
@@ -309,7 +310,7 @@ writeInChunksOfS n h m = writeArraysS h $ A.arraysOf n m
309310-- @since 0.7.0
310311{-# INLINE writeInChunksOf #-}
311312writeInChunksOf :: MonadIO m => Int -> Socket -> Fold m Word8 ()
312−writeInChunksOf n h = FL.lchunksOf n (A.writeN n) (writeArrays h)
313+writeInChunksOf n h = FL.lchunksOf n (A.writeNUnsafe n) (writeArrays h)
313314
314315-- > write = 'writeInChunksOf' A.defaultChunkSize
315316--
src/Streamly/Prelude/Internal.hs+15−1
@@ -260,6 +260,7 @@ module Streamly.Prelude.Internal
260260
261261 -- -- *** Chunks
262262 , chunksOf
263+ , arraysOf
263264 , intervalsOf
264265
265266 -- -- *** Using Element Separators
@@ -381,7 +382,7 @@ import qualified System.IO as IO
381382import Streamly.Enumeration (Enumerable(..), enumerate, enumerateTo)
382383import Streamly.Fold.Types (Fold (..))
383384import Streamly.Unfold.Types (Unfold)
384−import Streamly.Memory.Array.Types (Array)
385+import Streamly.Memory.Array.Types (Array, writeNUnsafe)
385386-- import Streamly.Memory.Ring (Ring)
386387import Streamly.SVar (MonadAsync, defState)
387388import Streamly.Streams.Async (mkAsync')
@@ -2247,6 +2248,19 @@ chunksOf
22472248 => Int -> Fold m a b -> t m a -> t m b
22482249chunksOf n f m = D.fromStreamD $ D.groupsOf n f (D.toStreamD m)
22492250
2251+-- | @arraysOf n stream@ groups the elements in the input stream into arrays of
2252+-- @n@ elements each.
2253+--
2254+-- Same as the following but may be more efficient:
2255+--
2256+-- > arraysOf n = S.chunksOf n (A.writeN n)
2257+--
2258+-- @since 0.7.0
2259+{-# INLINE arraysOf #-}
2260+arraysOf :: (IsStream t, MonadIO m, Storable a)
2261+ => Int -> t m a -> t m (Array a)
2262+arraysOf n = chunksOf n (writeNUnsafe n)
2263+
22502264-- XXX we can implement this by repeatedly applying the 'lrunFor' fold.
22512265-- XXX add this example after fixing the serial stream rate control
22522266-- >>> S.toList $ S.take 5 $ intervalsOf 1 FL.sum $ constRate 2 $ S.enumerateFrom 1
src/Streamly/Streams/StreamD/Type.hs+30−53
@@ -475,43 +475,13 @@ take n (Stream step state) = n `seq` Stream step' (state, 0)
475475-- Grouping/Splitting
476476------------------------------------------------------------------------------
477477
478−{-# INLINE_LATE foldOneGroup #-}
479−foldOneGroup
480− :: Monad m
481− => Int
482− -> Fold m a b
483− -> a
484− -> State K.Stream m a
485− -> (State K.Stream m a -> s -> m (Step s a))
486− -> s
487− -> m (b, Maybe s)
488−foldOneGroup n (Fold fstep begin done) x gst step state = do
489− acc0 <- begin
490− acc <- fstep acc0 x
491− go SPEC state acc 1
478+-- s = stream state, fs = fold state
479+data GroupState s fs
480+ = GroupStart s
481+ | GroupBuffer s fs Int
482+ | GroupYield fs (GroupState s fs)
483+ | GroupFinish
492484
493− where
494−
495− -- XXX is it strict enough?
496− go !_ st !acc i | i < n = do
497− r <- step gst st
498− case r of
499− Yield y s -> do
500− acc1 <- fstep acc y
501− go SPEC s acc1 (i + 1)
502− Skip s -> go SPEC s acc i
503− Stop -> do
504− res <- done acc
505− return (res, Nothing)
506− go !_ st acc _ = do
507− r <- done acc
508− return (r, Just st)
509−
510−-- groupsOf takes 11 sec to write the file from a stream, whereas just
511−-- reading in the same file as a stream and folding each element of the stream
512−-- using (+) takes just 1.5 sec, so we are still pretty slow (7x slow), there
513−-- is scope to make it faster. There is a possibility of better fusion here.
514−--
515485{-# INLINE_NORMAL groupsOf #-}
516486groupsOf
517487 :: Monad m
@@ -519,31 +489,38 @@ groupsOf
519489 -> Fold m a b
520490 -> Stream m a
521491 -> Stream m b
522−groupsOf n f (Stream step state) =
523− n `seq` Stream stepOuter (Just state)
492+groupsOf n (Fold fstep initial extract) (Stream step state) =
493+ n `seq` Stream step' (GroupStart state)
524494
525495 where
526496
527− {-# INLINE_LATE stepOuter #-}
528− stepOuter gst (Just st) = do
497+ {-# INLINE_LATE step' #-}
498+ step' _ (GroupStart st) = do
529499 -- XXX shall we use the Natural type instead? Need to check performance
530500 -- implications.
531501 when (n <= 0) $
532502 -- XXX we can pass the module string from the higher level API
533503 error $ "Streamly.Streams.StreamD.Type.groupsOf: the size of "
534504 ++ "groups [" ++ show n ++ "] must be a natural number"
505+ -- fs = fold state
506+ fs <- initial
507+ return $ Skip (GroupBuffer st fs 0)
535508
536− -- We retrieve the first element of the stream before we start to fold
537− -- a chunk so that we do not return an empty chunk in case the stream
538− -- is empty.
539− res <- step (adaptState gst) st
540− case res of
509+ step' gst (GroupBuffer st fs i) = do
510+ r <- step (adaptState gst) st
511+ case r of
541512 Yield x s -> do
542− -- XXX how to make sure that stepInner and f get fused
543− -- This problem seems to be similar to the concatMap problem
544− (r, s1) <- foldOneGroup n f x (adaptState gst) step s
545− return $ Yield r s1
546− Skip s -> return $ Skip $ Just s
547− Stop -> return Stop
548−
549− stepOuter _ Nothing = return Stop
513+ fs' <- fstep fs x
514+ let i' = i + 1
515+ return $
516+ if i' >= n
517+ then Skip (GroupYield fs' (GroupStart s))
518+ else Skip (GroupBuffer s fs' i')
519+ Skip s -> return $ Skip (GroupBuffer s fs i)
520+ Stop -> return $ Skip (GroupYield fs GroupFinish)
521+
522+ step' _ (GroupYield fs next) = do
523+ r <- extract fs
524+ return $ Yield r next
525+
526+ step' _ GroupFinish = return Stop
test/Arrays.hs+3−2
@@ -13,6 +13,7 @@ import Test.Hspec as H
1313
1414import qualified Streamly.Memory.Array as A
1515import qualified Streamly.Prelude as S
16+import qualified Streamly.Internal as Internal
1617
1718-- Coverage build takes too long with default number of tests
1819maxTestCount :: Int
@@ -74,7 +75,7 @@ testArraysOf =
7475 monadicIO $ do
7576 xs <- S.toList
7677 $ S.concatMap A.read
77− $ A.arraysOf 240
78+ $ Internal.arraysOf 240
7879 $ S.fromList list
7980 assert (xs == list)
8081
@@ -85,7 +86,7 @@ testFlattenArrays =
8586 monadicIO $ do
8687 xs <- S.toList
8788 $ A.concat
88− $ A.arraysOf 240
89+ $ Internal.arraysOf 240
8990 $ S.fromList list
9091 assert (xs == list)
9192