on master help sign in
Implement min/max/range without dequeue
d36f02fHarendra Kumar
core/src/Streamly/Internal/Data/Fold/Window.hs+74−110
@@ -51,17 +51,18 @@ module Streamly.Internal.Data.Fold.Window
51 )
51 )
52where
52where
53
53
54+import Control.Monad.IO.Class (MonadIO (liftIO))
54import Data.Bifunctor(bimap)
55import Data.Bifunctor(bimap)
55−import Data.Function ((&))
56+import Foreign (Storable(..))
56−import Data.Maybe (fromMaybe)
57
57
58import Streamly.Internal.Data.Fold.Type (Fold(..), Step(..))
58import Streamly.Internal.Data.Fold.Type (Fold(..), Step(..))
59−import Streamly.Internal.Data.Tuple.Strict (Tuple'(..), Tuple3'(..))
59+import Streamly.Internal.Data.Tuple.Strict
60+ (Tuple'(..), Tuple3Fused' (Tuple3Fused'))
60
61
61import Prelude hiding (length, sum, minimum, maximum)
62import Prelude hiding (length, sum, minimum, maximum)
62
63
63−import qualified Deque.Strict as DQ
64import qualified Streamly.Data.Fold as Fold
64import qualified Streamly.Data.Fold as Fold
65+import qualified Streamly.Internal.Data.Ring.Foreign as Ring
65
66
66-- $setup
67-- $setup
67-- >>> import Data.Bifunctor(bimap)
68-- >>> import Data.Bifunctor(bimap)
@@ -213,111 +214,90 @@ powerSumFrac p = lmap (** p) sum
213-- Location
214-- Location
214-------------------------------------------------------------------------------
215-------------------------------------------------------------------------------
215
216
216−-- Theoretically, we can approximate minimum in a rolling window by using a
217+-- XXX Remove MonadIO constraint
217−-- 'powerMean' with sufficiently large negative power.
218+
218−--
219+-- | Determine the maximum and minimum in a rolling window.
219−-- XXX If we need to know the minimum in the window only once in a while then
220−-- we can use linear search when it is extracted and not pay the cost all the
221−-- time.
222--
220--
223−-- | The minimum element in a rolling window.
221+-- If you want to compute the range of the entire stream @Fold.teeWith (,)
222+-- Fold.maximum Fold.minimum@ would be much faster.
224--
223--
225−-- If you want to compute the minimum of the entire stream Fold.minimum from
224+-- /Space/: \(\mathcal{O}(n)\) where @n@ is the window size.
226−-- streamly package would be much faster.
227--
225--
228-- /Time/: \(\mathcal{O}(n*w)\) where \(w\) is the window size.
226-- /Time/: \(\mathcal{O}(n*w)\) where \(w\) is the window size.
229--
227--
230−{-# INLINE minimum #-}
228+{-# INLINE range #-}
231−minimum :: (Monad m, Ord a) => Fold m (a, Maybe a) a
229+range :: (MonadIO m, Storable a, Ord a) => Int -> Fold m a (Maybe (a, a))
232−minimum = Fold step initial extract
230+range n = Fold step initial extract
233
231
234 where
232 where
235
233
236− initial = return $ Partial $ Tuple3' (0 :: Int) (0 :: Int)
234+ -- XXX Use Ring unfold and then fold for composing maximum and minimum to
237− (mempty :: DQ.Deque (Int, a))
235+ -- get the range.
238−
236+
239− step (Tuple3' i w q) (a, ma) =
237+ initial =
240− case ma of
238+ if n <= 0
241− Nothing ->
239+ then error "range: window size must be > 0"
240+ else
241+ let f (a, b) = Partial $ Tuple3Fused' a b (0 :: Int)
242+ in fmap f $ liftIO $ Ring.new n
243+
244+ step (Tuple3Fused' rb rh i) a = do
245+ rh1 <- liftIO $ Ring.unsafeInsert rb rh a
242− return $ Partial $ Tuple3' (i + 1) (w + 1)
246+ return $ Partial $ Tuple3Fused' rb rh1 (i + 1)
243− (headCheck i q (w + 1) & dqloop (i, a))
247+
244− Just _ ->
248+ -- XXX We need better Ring array APIs so that we can unfold the ring to a
245− return $ Partial $ Tuple3' (i + 1) w
249+ -- stream and fold the stream using a fold of our choice.
246− (headCheck i q w & dqloop (i,a))
250+ --
247−
251+ -- We could just scan the stream to get a stream of ring buffers and then
248− {-# INLINE headCheck #-}
252+ -- map required folds over those, but we need to be careful that all those
249− headCheck i q w =
253+ -- rings refer to the same mutable ring, therefore, downstream needs to
250− case DQ.uncons q of
254+ -- process those strictly before it can change.
251− Nothing -> q
255+ foldFunc i
252− Just (ia', q') ->
256+ | i < n = Ring.unsafeFoldRingM
253− if fst ia' <= i - w
257+ | otherwise = Ring.unsafeFoldRingFullM
254− then q'
258+
255− else q
259+ extract (Tuple3Fused' rb rh i) =
256−
260+ if i == 0
257− dqloop ia q =
261+ then return Nothing
258− case DQ.unsnoc q of
262+ else do
259− Nothing -> DQ.snoc ia q
263+ x <- liftIO $ peek rh
260− -- XXX This can be improved for the case of `=`
264+ let accum (mn, mx) a = return (min mn a, max mx a)
261− Just (ia', q') ->
265+ fmap Just $ foldFunc i rh accum (x, x) rb
262− if snd ia <= snd ia'
266+
263− then dqloop ia q'
267+-- | Find the minimum element in a rolling window.
264− else DQ.snoc ia q
268+--
265−
269+-- This implementation traverses the entire window buffer to compute the
266− extract (Tuple3' _ _ q) = return $ snd
270+-- minimum whenever we demand it. It performs better than the dequeue based
267− $ fromMaybe (0, error "min: Empty stream")
271+-- implementation in @streamly-statistics@ package when any of the following
268− $ DQ.head q
272+-- holds:
269−
273+--
270−-- Theoretically, we can approximate maximum in a rolling window by using a
274+-- * window size is small (< 30)
271−-- 'powerMean' with sufficiently large positive power.
275+-- * we are using this as a fold instead of a scan.
276+--
277+-- For other cases the implementation in the @streamly-statistics@ package
278+-- performs better.
279+--
280+-- If you want to compute the minimum of the entire stream
281+-- 'Streamly.Data.Fold.minimum' is much faster.
282+--
283+-- /Time/: \(\mathcal{O}(n*w)\) where \(w\) is the window size.
272--
284--
285+{-# INLINE minimum #-}
286+minimum :: (MonadIO m, Storable a, Ord a) => Int -> Fold m a (Maybe a)
287+minimum n = fmap (fmap fst) $ range n
288+
273-- | The maximum element in a rolling window.
289-- | The maximum element in a rolling window.
274--
290--
291+-- See the performance related comments in 'minimum'.
292+--
275−-- If you want to compute the maximum of the entire stream Fold.maximum from
293+-- If you want to compute the maximum of the entire stream 'Fold.maximum' would
276−-- streamly package would be much faster.
294+-- be much faster.
277--
295--
278-- /Time/: \(\mathcal{O}(n*w)\) where \(w\) is the window size.
296-- /Time/: \(\mathcal{O}(n*w)\) where \(w\) is the window size.
279--
297--
280{-# INLINE maximum #-}
298{-# INLINE maximum #-}
281−maximum :: (Monad m, Ord a) => Fold m (a, Maybe a) a
299+maximum :: (MonadIO m, Storable a, Ord a) => Int -> Fold m a (Maybe a)
282−maximum = Fold step initial extract
300+maximum n = fmap (fmap snd) $ range n
283−
284− where
285−
286− initial = return $ Partial $ Tuple3' (0 :: Int) (0 :: Int)
287− (mempty :: DQ.Deque (Int, a))
288−
289− step (Tuple3' i w q) (a, ma) =
290− case ma of
291− Nothing ->
292− return $ Partial $ Tuple3' (i + 1) (w + 1)
293− (headCheck i q (w + 1) & dqloop (i, a))
294− Just _ ->
295− return $ Partial $ Tuple3' (i + 1) w
296− (headCheck i q w & dqloop (i,a))
297−
298− {-# INLINE headCheck #-}
299− headCheck i q w =
300− case DQ.uncons q of
301− Nothing -> q
302− Just (ia', q') ->
303− if fst ia' <= i - w
304− then q'
305− else q
306−
307− dqloop ia q =
308− case DQ.unsnoc q of
309− Nothing -> DQ.snoc ia q
310− -- XXX This can be improved for the case of `=`
311− Just (ia', q') ->
312− if snd ia >= snd ia'
313− then dqloop ia q'
314− else DQ.snoc ia q
315−
316− extract (Tuple3' _ _ q) =
317− return
318− $ snd
319− $ fromMaybe (0, error "max: Empty stream")
320− $ DQ.head q
321
301
322-- | Arithmetic mean of elements in a sliding window:
302-- | Arithmetic mean of elements in a sliding window:
323--
303--
@@ -335,19 +315,3 @@ maximum = Fold step initial extract
335{-# INLINE mean #-}
315{-# INLINE mean #-}
336mean :: forall m a. (Monad m, Fractional a) => Fold m (a, Maybe a) a
316mean :: forall m a. (Monad m, Fractional a) => Fold m (a, Maybe a) a
337mean = Fold.teeWith (/) sum length
317mean = Fold.teeWith (/) sum length
338−
339−
340−-- | The difference between the maximum and minimum elements of a rolling window.
341−--
342−-- >>> range = Fold.teeWith (-) maximum minimum
343−--
344−-- If you want to compute the range of the entire stream @Fold.teeWith (-)
345−-- Fold.maximum Fold.min@ from the streamly package would be much faster.
346−--
347−-- /Space/: \(\mathcal{O}(n)\) where @n@ is the window size.
348−--
349−-- /Time/: \(\mathcal{O}(n*w)\) where \(w\) is the window size.
350−--
351−{-# INLINE range #-}
352−range :: (Monad m, Num a, Ord a) => Fold m (a, Maybe a) a
353−range = Fold.teeWith (-) maximum minimum
core/streamly-core.cabal−1
@@ -343,7 +343,6 @@ library
343 , transformers-base >= 0.4 && < 0.5
343 , transformers-base >= 0.4 && < 0.5
344 , primitive >= 0.5.4 && < 0.8
344 , primitive >= 0.5.4 && < 0.8
345 , heaps >= 0.3 && < 0.5
345 , heaps >= 0.3 && < 0.5
346− , deque >= 0.4 && < 0.5
347 , hashable >= 1.3 && < 1.5
346 , hashable >= 1.3 && < 1.5
348 , unordered-containers >= 0.2 && < 0.3
347 , unordered-containers >= 0.2 && < 0.3
349 if flag(use-unliftio)
348 if flag(use-unliftio)
test/Streamly/Test/Data/Fold/Window.hs+11−10
@@ -33,7 +33,9 @@ main = hspec $ do
33
33
34 describe "Correctness" $ do
34 describe "Correctness" $ do
35 let winSize = 3
35 let winSize = 3
36− testCase1 = [31, 41, 59, 26, 53, 58, 97] :: [Double]
36+ testCase1 =
37+ [1.0, 4.0, 3.0, 2.1, -5.1, -2.0, 7.0, 3.0, -2.5] :: [Double]
38+
37 testCase2 = replicate 5 1.0 ++ [7.0]
39 testCase2 = replicate 5 1.0 ++ [7.0]
38
40
39 testFunc tc f sI sW = do
41 testFunc tc f sI sW = do
@@ -44,18 +46,17 @@ main = hspec $ do
44 it "Infinite" $ a == sI
46 it "Infinite" $ a == sI
45 it ("Finite " ++ show winSize) $ b == sW
47 it ("Finite " ++ show winSize) $ b == sW
46
48
49+ testFunc2 tc expec f = do
50+ let c = S.fromList tc
51+ a <- runIO $ S.fold (f winSize) c
52+ it (show tc) $ a == expec
53+
47 describe "minimum" $ do
54 describe "minimum" $ do
48− let scanInf = [31, 31, 31, 26, 26, 26, 26] :: [Double]
49− scanWin = [31, 31, 31, 26, 26, 26, 53] :: [Double]
50− testFunc testCase1 minimum scanInf scanWin
55+ testFunc2 testCase1 (Just (-2.5)) minimum
51 describe "maximum" $ do
56 describe "maximum" $ do
52− let scanInf = [31, 41, 59, 59, 59, 59, 97] :: [Double]
53− scanWin = [31, 41, 59, 59, 59, 58, 97] :: [Double]
54− testFunc testCase1 maximum scanInf scanWin
57+ testFunc2 testCase1 (Just 7.0) maximum
55 describe "range" $ do
58 describe "range" $ do
56− let scanInf = [0, 10, 28, 33, 33, 33, 71] :: [Double]
57− scanWin = [0, 10, 28, 33, 33, 32, 44] :: [Double]
58− testFunc testCase1 range scanInf scanWin
59+ testFunc2 testCase1 (Just (-2.5, 7.0)) range
59 describe "sum" $ do
60 describe "sum" $ do
60 let scanInf = [1, 2, 3, 4, 5, 12] :: [Double]
61 let scanInf = [1, 2, 3, 4, 5, 12] :: [Double]
61 scanWin = [1, 2, 3, 3, 3, 9] :: [Double]
62 scanWin = [1, 2, 3, 3, 3, 9] :: [Double]