on master help sign in
Add word count example

Implement parallel and correct utf8 wc

commitb86b65ffe180d9cd54ee63627e95506884f5b23c authorHarendra Kumar <harendra.kumar@gmail.com> dateWed 23 Oct 2019, 21:14 (7 years ago) parenta9f3f9a
4 files changed+797−28
examples/WordCount.hs+627 src/Streamly/Data/String.hs+20 src/Streamly/Streams/StreamD.hs+125−28 streamly.cabal+25
examples/WordCount.hsnew file+627
@@ -0,0 +1,627 @@
1+-------------------------------------------------------------------------------
2+-- Fast, streaming and parallel word counting (wc) program.
3+-------------------------------------------------------------------------------
4+-- 1) On utf8 inputs the serial version is around 3x faster than MacOS wc
5+-- 2) It can run parallely on multiple cores providing further speedup
6+-- 3) Parallel version works efficiently on stdin/streaming input as well
7+-- 4) Parallel version handles utf8 input correctly (including multi-byte space
8+-- chars) and gives the same output as the serial version on all inputs.
9+-- 5) There may be differences in word/char counts when there are invalid utf8
10+-- byte sequences present in the input because of different styles of error
11+-- handling.
12+
13+-------------------------------------------------------------------------------
14+-- Build with the following options:
15+-------------------------------------------------------------------------------
16+-- streamly optimization plugin is required for best performance
17+-- ghc -O2 -fplugin Plugin -fspec-constr-recursive=10 -fmax-worker-args=16
18+-- For concurrent version add: -threaded -with-rtsopts "-N"
19+
20+-------------------------------------------------------------------------------
21+-- Comparing with "wc -mwl" command:
22+-------------------------------------------------------------------------------
23+--
24+-- 1) To enable UTF8 with wc: export LANG=en_US.UTF-8; export LC_ALL=$LANG
25+-- 2) To test whether it is acutally using utf8, copy and paste this string
26+-- "U+1680 U+2000 U+2001 U+2002" and run "wc -mwl" on this. Without proper UTF8
27+-- handling word count would be 1, with proper UTF8 handling word count would
28+-- be 4. Note that the spaces in this string are not regular space chars they
29+-- are different unicode space chars.
30+
31+{-# LANGUAGE CPP #-}
32+
33+import Control.Monad (when)
34+import Data.Char (isSpace)
35+import Data.Word (Word8)
36+import GHC.Conc (numCapabilities)
37+import System.Environment (getArgs)
38+import System.IO (Handle, openFile, IOMode(..))
39+
40+import qualified Streamly as S
41+import qualified Streamly.Data.String as S
42+import qualified Streamly.FileSystem.Handle as FH
43+import qualified Streamly.Memory.Array as A
44+import qualified Streamly.Prelude as S
45+import qualified Data.Vector.Storable.Mutable as V
46+
47+-------------------------------------------------------------------------------
48+-- Parallel char, line and word counting
49+-------------------------------------------------------------------------------
50+
51+-- We process individual chunks in the stream independently and parallely and
52+-- the combine the chunks to combine what they have counted.
53+--
54+-------------------------------------------------------------------------------
55+-- Char counting
56+-------------------------------------------------------------------------------
57+
58+-- To count chars each block needs the following:
59+--
60+-- -- | header | char counts | trailer |
61+--
62+-- header and trailer are incomplete utf8 byte sequences that may be combined
63+-- with the previous or the next block to complete them later.
64+--
65+-- The trailer may have one or more bytes in a valid utf8 sequence and is
66+-- expecting more bytes to complete the sequence. The header stores any
67+-- possible continuation from the previous block. It contains a maximum of 3
68+-- bytes which all must be non-starter bytes.
69+--
70+-- When two blocks are combined, the trailer of the first block is combined
71+-- with the header of the next block and then utf8 decoded. The combined
72+-- header+trailer may yield:
73+--
74+-- * Nothing - when there is no trailer and header
75+-- * All errors - when there is no trailer in the previous block, and there is
76+-- a header in the next block. In this case there is no starting char which
77+-- means all header bytes are errors.
78+-- * It can yield at most one valid character followed by 0, 1 or 2 errors.
79+--
80+-- We count an incomplete utf8 sequence of 2 or more bytes starting with a
81+-- valid starter byte as a single codepoint. Bytes not following a valid
82+-- starter byte are treated as individual codepoints for counting.
83+--
84+-------------------------------------------------------------------------------
85+-- Word counting
86+-------------------------------------------------------------------------------
87+
88+-- For word counting we need the following in each block:
89+--
90+-- -- | header | startsWithSpace | word counts | endsWithSpace | trailer |
91+--
92+-- The word counts in individual blocks are performed assuming that the
93+-- previous char before the block is a space.
94+-- When combining two blocks, after combining the trailer of previous blocks
95+-- with the header of the next we determine if the resulting char is a space or
96+-- not.
97+--
98+-- 1) If there is no new char joining the two blocks then we use endsWithSpace
99+-- of the previous block and startsWithSpace of the next block to determine if
100+-- the word counts are to be adjusted. If the previous block ends with
101+-- non-space and the next block starts with non-space we need to decrement the
102+-- word count by one if it is non-zero in the next block.
103+--
104+-- 2) If the new joining char is a space then we combine it with
105+-- startsWithSpace and endsWithSpace to determine the
106+-- startsWithSpace/endsWithSpace of the combined block and adjust the word
107+-- counts appropriately.
108+--
109+-------------------------------------------------------------------------------
110+-- Line counting
111+-------------------------------------------------------------------------------
112+
113+-- Line counting is performed by counting "\n" in the stream. No new "\n" can
114+-- result from patching the trailer and header as it is always a single byte.
115+
116+-------------------------------------------------------------------------------
117+-- Counting state
118+-------------------------------------------------------------------------------
119+
120+-- We use a mutable vector for the counting state. A fold using an immutable
121+-- structure for such a large state does not perform well. However, mutability
122+-- is confined to just the accumulator.
123+--
124+-- XXX we need convenient mutable records (like C structs) to handle things
125+-- like this. It may be possible to achieve the same performance with an
126+-- immutable accumulator, but that will require more research. Since we are
127+-- always discarding the previous state, we can perhaps make use of that memory
128+-- using safe in-place modifications, without having to allocate new memory.
129+
130+-- XXX we can also count the number of decoding errors separately
131+data Field =
132+ -- The number of "\n" characters found in the block.
133+ LineCount
134+ -- Number of full words found in the block, words are counted on a
135+ -- transition from space char to a non-space char. We always assume the
136+ -- char before the first starter char in a block is a space. If this is
137+ -- found to be incorrect when joining two blocks then we fix the counts
138+ -- accordingly.
139+ | WordCount
140+ -- The number of successfully decoded characters plus the number of
141+ -- decoding failures in the block. Each byte or sequence of bytes on which
142+ -- decoding fails is also counted as one char. The header and trailer bytes
143+ -- are not accounted in this, they are accounted only when we join two
144+ -- blocks.
145+ | CharCount
146+ -- whether the last counted char in this block was a space char
147+ | WasSpace
148+ -- whether the first successfully decoded char in this block is a space. A
149+ -- decoding failure, after the trailing bytes from the previous block are
150+ -- accounted, is also considered as space.
151+ | FirstIsSpace
152+ -- If no starter byte is found in the first three bytes in the block then
153+ -- store those bytes to possibly combine them with the trailing incomplete
154+ -- byte sequence in the previous block. We mark it done when either we have
155+ -- stored three bytes or we have found a starter byte.
156+ --
157+ -- XXX This is ugly to manipulate, we can implement a statically max sized
158+ -- mutable ring structure within this record.
159+ | HeaderDone
160+ | HeaderWordCount
161+ | HeaderWord1
162+ | HeaderWord2
163+ | HeaderWord3
164+ -- If a byte sequence at the end of the block is not complete then store
165+ -- the current state of the utf8 decoder to continue it later using the
166+ -- incomplete leading byte sequence in the next block.
167+ | TrailerPresent
168+ | TrailerState
169+ | TrailerCodePoint
170+ deriving (Show, Enum, Bounded)
171+
172+-------------------------------------------------------------------------------
173+-- Default/initial state of the block
174+-------------------------------------------------------------------------------
175+
176+readField :: V.IOVector Int -> Field -> IO Int
177+readField v fld = V.read v (fromEnum fld)
178+
179+writeField :: V.IOVector Int -> Field -> Int -> IO ()
180+writeField v fld val = V.write v (fromEnum fld) val
181+
182+modifyField :: V.IOVector Int -> Field -> (Int -> Int) -> IO ()
183+modifyField v fld f = V.modify v f (fromEnum fld)
184+
185+newCounts :: IO (V.IOVector Int)
186+newCounts = do
187+ counts <- V.new (fromEnum (maxBound :: Field) + 1)
188+ writeField counts LineCount 0
189+ writeField counts WordCount 0
190+ writeField counts CharCount 0
191+ writeField counts WasSpace 1
192+ writeField counts FirstIsSpace 0
193+ writeField counts HeaderDone 0
194+ writeField counts HeaderWordCount 0
195+ writeField counts TrailerPresent 0
196+ return counts
197+
198+-------------------------------------------------------------------------------
199+-- Counting chars
200+-------------------------------------------------------------------------------
201+
202+accountChar :: V.IOVector Int -> Bool -> IO ()
203+accountChar counts isSp = do
204+ c <- readField counts CharCount
205+ let space = if isSp then 1 else 0
206+ when (c == 0) $ writeField counts FirstIsSpace space
207+ writeField counts CharCount (c + 1)
208+ writeField counts WasSpace space
209+
210+-------------------------------------------------------------------------------
211+-- Manipulating the header bytes
212+-------------------------------------------------------------------------------
213+
214+addToHeader :: V.IOVector Int -> Int -> IO Bool
215+addToHeader counts cp = do
216+ cnt <- readField counts HeaderWordCount
217+ case cnt of
218+ 0 -> do
219+ writeField counts HeaderWord1 cp
220+ writeField counts HeaderWordCount 1
221+ return True
222+ 1 -> do
223+ writeField counts HeaderWord2 cp
224+ writeField counts HeaderWordCount 2
225+ return True
226+ 2 -> do
227+ writeField counts HeaderWord3 cp
228+ writeField counts HeaderWordCount 3
229+ writeField counts HeaderDone 1
230+ return True
231+ _ -> return False
232+
233+resetHeaderOnNewChar :: V.IOVector Int -> IO ()
234+resetHeaderOnNewChar counts = do
235+ hdone <- readField counts HeaderDone
236+ when (hdone == 0) $ writeField counts HeaderDone 1
237+
238+-------------------------------------------------------------------------------
239+-- Manipulating the trailer
240+-------------------------------------------------------------------------------
241+
242+setTrailer :: V.IOVector Int -> S.DecodeState -> S.CodePoint -> IO ()
243+setTrailer counts st cp = do
244+ writeField counts TrailerState (fromIntegral st)
245+ writeField counts TrailerCodePoint cp
246+ writeField counts TrailerPresent 1
247+
248+resetTrailerOnNewChar :: V.IOVector Int -> IO ()
249+resetTrailerOnNewChar counts = do
250+ trailer <- readField counts TrailerPresent
251+ when (trailer /= 0) $ do
252+ writeField counts TrailerPresent 0
253+ accountChar counts True
254+
255+-------------------------------------------------------------------------------
256+-- Counting the stream
257+-------------------------------------------------------------------------------
258+
259+{-# INLINE countChar #-}
260+countChar :: V.IOVector Int -> Either S.DecodeError Char -> IO ()
261+countChar counts inp =
262+ case inp of
263+ Right ch -> do
264+ resetHeaderOnNewChar counts
265+ -- account the last stored error as whitespace and clear it
266+ resetTrailerOnNewChar counts
267+
268+ when (ch == '\n') $ modifyField counts LineCount (+ 1)
269+ if isSpace ch
270+ then accountChar counts True
271+ else do
272+ wasSpace <- readField counts WasSpace
273+ when (wasSpace /= 0) $ modifyField counts WordCount (+ 1)
274+ accountChar counts False
275+ Left (S.DecodeError st cp) -> do
276+ hdone <- readField counts HeaderDone
277+ if hdone == 0
278+ then do
279+ if st == 0
280+ then do
281+ -- We got a non-starter in initial decoder state, there may
282+ -- be something that comes before this to complete it.
283+ r <- addToHeader counts cp
284+ when (not r) $ error "countChar: Bug addToHeader failed"
285+ else do
286+ -- We got an error in a non-initial decoder state, it may
287+ -- be an input underflow error, keep it as incomplete in
288+ -- the trailer.
289+ writeField counts HeaderDone 1
290+ setTrailer counts st cp
291+ else do
292+ resetTrailerOnNewChar counts
293+ if st == 0
294+ then accountChar counts True
295+ else setTrailer counts st cp
296+
297+printCounts :: V.IOVector Int -> IO ()
298+printCounts v = do
299+ l <- readField v LineCount
300+ w <- readField v WordCount
301+ c <- readField v CharCount
302+ putStrLn $ show l ++ " " ++ show w ++ " " ++ show c
303+
304+-------------------------------------------------------------------------------
305+-- Serial counting using parallel version of countChar
306+-------------------------------------------------------------------------------
307+
308+_wc_mwl_parserial :: Handle -> IO (V.IOVector Int)
309+_wc_mwl_parserial src = do
310+ counts <- newCounts
311+ S.mapM_ (countChar counts)
312+ $ S.decodeUtf8Either
313+ $ S.unfold FH.read src
314+ return counts
315+
316+-------------------------------------------------------------------------------
317+-- Serial word counting with UTF-8 handling
318+-------------------------------------------------------------------------------
319+
320+data Counts = Counts !Int !Int !Int !Bool deriving Show
321+
322+{-# INLINE countCharSerial #-}
323+countCharSerial :: Counts -> Char -> Counts
324+countCharSerial (Counts l w c wasSpace) ch =
325+ let l1 = if (ch == '\n') then l + 1 else l
326+ (w1, wasSpace1) =
327+ if (isSpace ch)
328+ then (w, True)
329+ else (if wasSpace then w + 1 else w, False)
330+ in (Counts l1 w1 (c + 1) wasSpace1)
331+
332+-- Note: This counts invalid byte sequences are non-space chars
333+_wc_mwl_serial :: Handle -> IO ()
334+_wc_mwl_serial src = print =<< (
335+ S.foldl' countCharSerial (Counts 0 0 0 True)
336+ $ S.decodeUtf8Lenient
337+ $ S.unfold FH.read src)
338+
339+-------------------------------------------------------------------------------
340+-- Parallel counting
341+-------------------------------------------------------------------------------
342+
343+-- XXX we need a better data structure to store the header bytes to make these
344+-- routines simpler.
345+--
346+-- combine trailing bytes in preceding block with leading bytes in the next
347+-- block and decode them into a codepoint
348+reconstructChar :: Int
349+ -> V.IOVector Int
350+ -> V.IOVector Int
351+ -> IO (S.SerialT IO (Either S.DecodeError Char))
352+reconstructChar hdrCnt v1 v2 = do
353+ when (hdrCnt > 3 || hdrCnt < 0) $ error "reconstructChar: hdrCnt > 3"
354+ stream1 <-
355+ if (hdrCnt > 2)
356+ then do
357+ x <- readField v2 HeaderWord3
358+ return $ (fromIntegral x :: Word8) `S.cons` S.nil
359+ else return S.nil
360+ stream2 <-
361+ if (hdrCnt > 1)
362+ then do
363+ x <- readField v2 HeaderWord2
364+ return $ fromIntegral x `S.cons` stream1
365+ else return stream1
366+ stream3 <-
367+ if (hdrCnt > 0)
368+ then do
369+ x <- readField v2 HeaderWord1
370+ return $ fromIntegral x `S.cons` stream2
371+ else return stream2
372+
373+ state <- readField v1 TrailerState
374+ cp <- readField v1 TrailerCodePoint
375+ return $ S.resumeDecodeUtf8Either (fromIntegral state) cp stream3
376+
377+getHdrChar :: V.IOVector Int -> IO (Maybe Int)
378+getHdrChar v = do
379+ hdrCnt <- readField v HeaderWordCount
380+ case hdrCnt of
381+ 0 -> return Nothing
382+ 1 -> do
383+ writeField v HeaderWordCount 0
384+ fmap Just $ readField v HeaderWord1
385+ 2 -> do
386+ x1 <- readField v HeaderWord1
387+ x2 <- readField v HeaderWord2
388+ writeField v HeaderWord1 x2
389+ writeField v HeaderWordCount 1
390+ return $ Just x1
391+ 3 -> do
392+ x1 <- readField v HeaderWord1
393+ x2 <- readField v HeaderWord2
394+ x3 <- readField v HeaderWord3
395+ writeField v HeaderWord1 x2
396+ writeField v HeaderWord2 x3
397+ writeField v HeaderWordCount 2
398+ return $ Just x1
399+ _ -> error "getHdrChar: Bug, hdrCnt not in range 0-3"
400+
401+-- If the header of the first block is not done then combine the header
402+-- with the header of the next block.
403+combineHeaders :: V.IOVector Int -> V.IOVector Int -> IO ()
404+combineHeaders v1 v2 = do
405+ hdone1 <- readField v1 HeaderDone
406+ if hdone1 == 0
407+ then do
408+ res <- getHdrChar v2
409+ case res of
410+ Nothing -> return ()
411+ Just x -> do
412+ r <- addToHeader v1 x
413+ when (not r) $ error "combineHeaders: Bug, addToHeader failed"
414+ else return ()
415+
416+-- We combine the contents of the second vector into the first vector, mutating
417+-- the first vector and returning it.
418+-- XXX This is a quick hack and can be refactored to reduce the size
419+-- and understandability considerably.
420+addCounts :: V.IOVector Int -> V.IOVector Int -> IO (V.IOVector Int)
421+addCounts v1 v2 = do
422+ hdone1 <- readField v1 HeaderDone
423+ hdone2 <- readField v2 HeaderDone
424+ hdrCnt2_0 <- readField v2 HeaderWordCount
425+ if hdone1 == 0 && (hdrCnt2_0 /= 0 || hdone2 /= 0)
426+ then do
427+ combineHeaders v1 v2
428+ if hdone2 == 0
429+ then return v1
430+ else do
431+ writeField v1 HeaderDone 1
432+ addCounts v1 v2
433+ else do
434+ trailerPresent2 <- readField v2 TrailerPresent
435+ trailerState2 <- readField v2 TrailerState
436+ trailerCodePoint2 <- readField v2 TrailerCodePoint
437+ if hdone1 == 0
438+ then error "addCounts: Bug, trying to add completely empty second block"
439+ else do
440+ l1 <- readField v1 LineCount
441+ w1 <- readField v1 WordCount
442+ c1 <- readField v1 CharCount
443+ wasSpace1 <- readField v1 WasSpace
444+
445+ l2 <- readField v2 LineCount
446+ w2 <- readField v2 WordCount
447+ c2 <- readField v2 CharCount
448+ wasSpace2 <- readField v2 WasSpace
449+ firstIsSpace2 <- readField v2 FirstIsSpace
450+ hdrCnt2 <- readField v2 HeaderWordCount
451+
452+ trailer1 <- readField v1 TrailerPresent
453+ if trailer1 == 0 -- no trailer in the first block
454+ then do
455+ -- header2, if any, complete or incomplete, is just invalid
456+ -- bytes, count them as whitespace
457+ let firstIsSpace2' = firstIsSpace2 /= 0 || hdrCnt2 /= 0
458+ w <-
459+ if w2 /= 0 && wasSpace1 == 0 && not firstIsSpace2'
460+ then return $ w1 + w2 - 1
461+ else return $ w1 + w2
462+ writeField v1 LineCount (l1 + l2)
463+ writeField v1 WordCount w
464+ writeField v1 CharCount (c1 + c2 + hdrCnt2)
465+
466+ when (c1 == 0) $ do
467+ if c2 == 0 && hdrCnt2 /= 0
468+ then writeField v1 FirstIsSpace 1
469+ else writeField v1 FirstIsSpace firstIsSpace2
470+
471+ if c2 == 0 && hdrCnt2 /= 0
472+ then writeField v1 WasSpace 1
473+ else writeField v1 WasSpace wasSpace2
474+
475+ writeField v1 TrailerPresent trailerPresent2
476+ writeField v1 TrailerState trailerState2
477+ writeField v1 TrailerCodePoint trailerCodePoint2
478+ return v1
479+ else do
480+ if hdrCnt2 == 0
481+ then do
482+ when (hdone2 /= 0) $ do -- empty and Done header
483+ -- count trailer as whitespace, its counted as one char
484+ -- Note: hdrCnt2 == 0 means either header is not done
485+ -- or c2 /= 0
486+ writeField v1 LineCount (l1 + l2)
487+ writeField v1 WordCount (w1 + w2)
488+ writeField v1 CharCount (c1 + c2 + 1)
489+
490+ when (c1 == 0) $ writeField v1 FirstIsSpace 1
491+ if (c2 == 0)
492+ then writeField v1 WasSpace 1
493+ else writeField v1 WasSpace wasSpace2
494+
495+ writeField v1 TrailerPresent trailerPresent2
496+ writeField v1 TrailerState trailerState2
497+ writeField v1 TrailerCodePoint trailerCodePoint2
498+ -- If header of the next block is not done we continue the
499+ -- trailer from the previous block instead of treating it
500+ -- as whitespace
501+ return v1
502+ else do
503+ -- join the trailing part of the first block with the
504+ -- header of the next
505+ decoded <- reconstructChar hdrCnt2 v1 v2
506+ res <- S.uncons decoded
507+ case res of
508+ Nothing -> error "addCounts: Bug. empty reconstructed char"
509+ Just (h, t) -> do
510+ tlength <- S.length t
511+ case h of
512+ Right ch -> do
513+ -- If we have an error case after this
514+ -- char then that would be treated
515+ -- as whitespace
516+ let lcount = l1 + l2
517+ lcount1 = if (ch == '\n') then lcount + 1 else lcount
518+ wcount = w1 + w2
519+ firstSpace = isSpace ch
520+ wasSpace = firstSpace || tlength /= 0
521+ wcount1 =
522+ if wasSpace
523+ then wcount
524+ else if w2 == 0 || firstIsSpace2 /= 0
525+ then wcount
526+ else wcount - 1
527+ writeField v1 LineCount lcount1
528+ writeField v1 WordCount wcount1
529+ writeField v1 CharCount (c1 + c2 + 1 + tlength)
530+
531+ when (c1 == 0) $
532+ writeField v1 FirstIsSpace
533+ (if firstSpace then 1 else 0)
534+
535+ if c2 == 0
536+ then do
537+ if wasSpace
538+ then writeField v1 WasSpace 1
539+ else writeField v1 WasSpace 0
540+ else writeField v1 WasSpace wasSpace2
541+
542+ writeField v1 TrailerPresent trailerPresent2
543+ writeField v1 TrailerState trailerState2
544+ writeField v1 TrailerCodePoint trailerCodePoint2
545+ return v1
546+ Left (S.DecodeError st cp) -> do
547+ -- if header was incomplete it may result
548+ -- in partially decoded char to be written
549+ -- as trailer. Check if the last error is
550+ -- an incomplete decode.
551+ r <- S.last t
552+ let (st', cp') =
553+ case r of
554+ Nothing -> (st, cp)
555+ Just lst -> case lst of
556+ Right _ -> error "addCounts: Bug"
557+ Left (S.DecodeError st1 cp1) -> (st1, cp1)
558+ if hdone2 == 0 && st' /= 12
559+ then do
560+ -- all elements before the last one must be errors
561+ writeField v1 CharCount (c1 + tlength)
562+
563+ when (c1 == 0) $
564+ writeField v1 FirstIsSpace 1
565+
566+ writeField v1 WasSpace 1
567+
568+ writeField v1 TrailerState (fromIntegral st')
569+ writeField v1 TrailerCodePoint cp'
570+ else do
571+ -- all elements must be errors
572+ -- treat them as whitespace
573+ writeField v1 LineCount (l1 + l2)
574+ writeField v1 WordCount (w1 + w2)
575+ writeField v1 CharCount (c1 + c2 + tlength + 1)
576+
577+ when (c1 == 0) $
578+ writeField v1 FirstIsSpace 1
579+
580+ if c2 == 0
581+ then writeField v1 WasSpace 1
582+ else writeField v1 WasSpace wasSpace2
583+
584+ writeField v1 TrailerPresent trailerPresent2
585+ writeField v1 TrailerState trailerState2
586+ writeField v1 TrailerCodePoint trailerCodePoint2
587+ return v1
588+
589+-- Individual array processing is an isolated loop, fusing it with the bigger
590+-- loop may be counter productive.
591+{-# NOINLINE countArray #-}
592+countArray :: A.Array Word8 -> IO (V.IOVector Int)
593+countArray src = do
594+ counts <- newCounts
595+ S.mapM_ (countChar counts)
596+ $ S.decodeUtf8Either
597+ $ S.unfold A.read src
598+ return counts
599+
600+{-# INLINE wc_mwl_parallel #-}
601+wc_mwl_parallel :: Handle -> Int -> IO (V.IOVector Int)
602+wc_mwl_parallel src n = do
603+ counts <- newCounts
604+ S.foldlM' addCounts counts
605+ $ S.aheadly
606+ $ S.maxThreads numCapabilities
607+ $ S.mapM (countArray)
608+ $ S.unfold FH.readArraysOf (n, src)
609+
610+-------------------------------------------------------------------------------
611+-- Main
612+-------------------------------------------------------------------------------
613+
614+main :: IO ()
615+main = do
616+ name <- fmap head getArgs
617+ src <- openFile name ReadMode
618+ -- _wc_mwl_serial src -- Unix wc -l program
619+ -- printCounts =<< _wc_mwl_parserial src -- Unix wc -l program
620+ -- Using different sizes of chunks (1,2,3,4,5,10,128,256) is a good testing
621+ -- mechanism for parallel counting code.
622+ {-
623+ args <- getArgs
624+ let chunkSize = read $ args !! 1
625+ -}
626+ let chunkSize = 32 * 1024
627+ printCounts =<< wc_mwl_parallel src chunkSize -- Unix wc -l program
src/Streamly/Data/String.hs+20
@@ -52,6 +52,11 @@ module Streamly.Data.String
5252 decodeChar8
5353 , decodeUtf8
5454 , decodeUtf8Lenient
55+ , D.DecodeError(..)
56+ , D.DecodeState
57+ , D.CodePoint
58+ , decodeUtf8Either
59+ , resumeDecodeUtf8Either
5560 , decodeUtf8Arrays
5661 , decodeUtf8ArraysLenient
5762
@@ -155,6 +160,21 @@ decodeUtf8Arrays = D.fromStreamD . D.decodeUtf8Arrays . D.toStreamD
155160decodeUtf8Lenient :: (Monad m, IsStream t) => t m Word8 -> t m Char
156161decodeUtf8Lenient = D.fromStreamD . D.decodeUtf8Lenient . D.toStreamD
157162
163+{-# INLINE decodeUtf8Either #-}
164+decodeUtf8Either :: (Monad m, IsStream t)
165+ => t m Word8 -> t m (Either D.DecodeError Char)
166+decodeUtf8Either = D.fromStreamD . D.decodeUtf8Either . D.toStreamD
167+
168+{-# INLINE resumeDecodeUtf8Either #-}
169+resumeDecodeUtf8Either
170+ :: (Monad m, IsStream t)
171+ => D.DecodeState
172+ -> D.CodePoint
173+ -> t m Word8
174+ -> t m (Either D.DecodeError Char)
175+resumeDecodeUtf8Either st cp =
176+ D.fromStreamD . D.resumeDecodeUtf8Either st cp . D.toStreamD
177+
158178{-# INLINE decodeUtf8ArraysLenient #-}
159179decodeUtf8ArraysLenient ::
160180 (MonadIO m, IsStream t) => t m (Array Word8) -> t m Char
src/Streamly/Streams/StreamD.hs+125−28
@@ -271,9 +271,14 @@ module Streamly.Streams.StreamD
271271 , handle
272272
273273 -- * UTF8 Encoding / Decoding transformations.
274+ , DecodeError(..)
275+ , DecodeState
276+ , CodePoint
274277 , decodeUtf8
275278 , encodeUtf8
276279 , decodeUtf8Lenient
280+ , decodeUtf8Either
281+ , resumeDecodeUtf8Either
277282 , decodeUtf8Arrays
278283 , decodeUtf8ArraysLenient
279284 )
@@ -3428,7 +3433,7 @@ replacementChar = '\xFFFD'
34283433
34293434-- Int helps in cheaper conversion from Int to Char
34303435type CodePoint = Int
3431−type DecoderState = Word8
3436+type DecodeState = Word8
34323437
34333438-- See http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ for details.
34343439
@@ -3436,6 +3441,8 @@ type DecoderState = Word8
34363441runFold :: (Monad m) => Fold m a b -> Stream m a -> m b
34373442runFold (Fold step begin done) = foldlMx' step begin done
34383443
3444+-- XXX Use names decodeSuccess = 0, decodeFailure = 12
3445+
34393446-- Aligning to cacheline makes a barely noticeable difference
34403447utf8d :: A.Array Word8
34413448utf8d = unsafePerformIO $ runFold (A.writeAligned 64) $ fromList [
@@ -3471,7 +3478,7 @@ unsafePeekElemOff p i = let !x = A.unsafeInlineIO $ peekElemOff p i in x
34713478--
34723479-- When the state is 0
34733480{-# INLINE decode0 #-}
3474−decode0 :: Ptr Word8 -> Word8 -> Tuple' DecoderState CodePoint
3481+decode0 :: Ptr Word8 -> Word8 -> Tuple' DecodeState CodePoint
34753482decode0 table byte =
34763483 let !t = table `unsafePeekElemOff` fromIntegral byte
34773484 !codep' = (0xff `shiftR` (fromIntegral t)) .&. fromIntegral byte
@@ -3482,10 +3489,10 @@ decode0 table byte =
34823489{-# INLINE decode1 #-}
34833490decode1
34843491 :: Ptr Word8
3485− -> DecoderState
3492+ -> DecodeState
34863493 -> CodePoint
34873494 -> Word8
3488− -> Tuple' DecoderState CodePoint
3495+ -> Tuple' DecodeState CodePoint
34893496decode1 table state codep byte =
34903497 -- Remember codep is Int type!
34913498 -- Can it be unsafe to convert the resulting to Char?
@@ -3495,15 +3502,23 @@ decode1 table state codep byte =
34953502 (256 + fromIntegral state + fromIntegral t)
34963503 in (Tuple' state' codep')
34973504
3505+-- We can divide the errors in three general categories:
3506+-- * A non-starter was encountered in a begin state
3507+-- * A starter was encountered without completing a codepoint
3508+-- * The last codepoint was not complete (input underflow)
3509+--
3510+data DecodeError = DecodeError !DecodeState !CodePoint deriving Show
34983511
3499−data FreshPoint s
3512+data FreshPoint s a
35003513 = FreshPointDecodeInit s
3514+ | FreshPointDecodeInit1 s Word8
35013515 | FreshPointDecodeFirst s Word8
3502− | FreshPointDecoding s !DecoderState !CodePoint
3503− | YieldAndContinue !Char (FreshPoint s)
3516+ | FreshPointDecoding s !DecodeState !CodePoint
3517+ | YieldAndContinue a (FreshPoint s a)
35043518 | Done
35053519
35063520-- XXX Add proper error messages
3521+-- XXX Implement this in terms of decodeUtf8Either
35073522{-# INLINE_NORMAL decodeUtf8With #-}
35083523decodeUtf8With :: Monad m => CodingFailureMode -> Stream m Word8 -> Stream m Char
35093524decodeUtf8With cfm (Stream step state) =
@@ -3525,25 +3540,28 @@ decodeUtf8With cfm (Stream step state) =
35253540 {-# INLINE_LATE step' #-}
35263541 step' _ gst (FreshPointDecodeInit st) = do
35273542 r <- step (adaptState gst) st
3528− case r of
3529− Yield x s ->
3530− -- Note: It is important to use a ">" instead of a "<=" test
3531− -- here for GHC to generate code layout for default branch
3532− -- prediction for the common case. This is fragile and might
3533− -- change with the compiler versions, we need a more reliable
3534− -- "likely" primitive to control branch predication.
3535− case x > 0x7f of
3536− False ->
3537− return $ Skip $ YieldAndContinue
3538− (unsafeChr (fromIntegral x))
3539− (FreshPointDecodeInit s)
3540− -- Using a separate state here generates a jump to a
3541− -- separate code block in the core which seems to perform
3542− -- slightly better for the non-ascii case.
3543− True -> return $ Skip $ FreshPointDecodeFirst s x
3544− Skip s -> return $ Skip (FreshPointDecodeInit s)
3545− Stop -> return $ Skip Done
3543+ return $ case r of
3544+ Yield x s -> Skip (FreshPointDecodeInit1 s x)
3545+ Skip s -> Skip (FreshPointDecodeInit s)
3546+ Stop -> Skip Done
3547+
3548+ step' _ _ (FreshPointDecodeInit1 st x) = do
3549+ -- Note: It is important to use a ">" instead of a "<=" test
3550+ -- here for GHC to generate code layout for default branch
3551+ -- prediction for the common case. This is fragile and might
3552+ -- change with the compiler versions, we need a more reliable
3553+ -- "likely" primitive to control branch predication.
3554+ case x > 0x7f of
3555+ False ->
3556+ return $ Skip $ YieldAndContinue
3557+ (unsafeChr (fromIntegral x))
3558+ (FreshPointDecodeInit st)
3559+ -- Using a separate state here generates a jump to a
3560+ -- separate code block in the core which seems to perform
3561+ -- slightly better for the non-ascii case.
3562+ True -> return $ Skip $ FreshPointDecodeFirst st x
35463563
3564+ -- XXX should we merge it with FreshPointDecodeInit1?
35473565 step' table _ (FreshPointDecodeFirst st x) = do
35483566 let (Tuple' sv cp) = decode0 table x
35493567 return $
@@ -3556,6 +3574,8 @@ decodeUtf8With cfm (Stream step state) =
35563574 0 -> error "unreachable state"
35573575 _ -> Skip (FreshPointDecoding st sv cp)
35583576
3577+ -- We recover by trying the new byte x a starter of a new codepoint.
3578+ -- XXX need to use the same recovery in array decoding routine as well
35593579 step' table gst (FreshPointDecoding st statePtr codepointPtr) = do
35603580 r <- step (adaptState gst) st
35613581 case r of
@@ -3569,7 +3589,7 @@ decodeUtf8With cfm (Stream step state) =
35693589 Skip $
35703590 transliterateOrError
35713591 "Streamly.Streams.StreamD.decodeUtf8With: Invalid UTF8 codepoint encountered"
3572− (FreshPointDecodeInit s)
3592+ (FreshPointDecodeInit1 s x)
35733593 _ -> Skip (FreshPointDecoding s sv cp)
35743594 Skip s -> return $ Skip (FreshPointDecoding s statePtr codepointPtr)
35753595 Stop -> return $ Skip inputUnderflow
@@ -3585,12 +3605,89 @@ decodeUtf8 = decodeUtf8With ErrorOnCodingFailure
35853605decodeUtf8Lenient :: Monad m => Stream m Word8 -> Stream m Char
35863606decodeUtf8Lenient = decodeUtf8With TransliterateCodingFailure
35873607
3608+{-# INLINE_NORMAL resumeDecodeUtf8Either #-}
3609+resumeDecodeUtf8Either
3610+ :: Monad m
3611+ => DecodeState
3612+ -> CodePoint
3613+ -> Stream m Word8
3614+ -> Stream m (Either DecodeError Char)
3615+resumeDecodeUtf8Either dst codep (Stream step state) =
3616+ let Array p _ _ = utf8d
3617+ !ptr = (unsafeForeignPtrToPtr p)
3618+ stt =
3619+ if dst == 0
3620+ then FreshPointDecodeInit state
3621+ else FreshPointDecoding state dst codep
3622+ in Stream (step' ptr) stt
3623+ where
3624+ {-# INLINE_LATE step' #-}
3625+ step' _ gst (FreshPointDecodeInit st) = do
3626+ r <- step (adaptState gst) st
3627+ return $ case r of
3628+ Yield x s -> Skip (FreshPointDecodeInit1 s x)
3629+ Skip s -> Skip (FreshPointDecodeInit s)
3630+ Stop -> Skip Done
3631+
3632+ step' _ _ (FreshPointDecodeInit1 st x) = do
3633+ -- Note: It is important to use a ">" instead of a "<=" test
3634+ -- here for GHC to generate code layout for default branch
3635+ -- prediction for the common case. This is fragile and might
3636+ -- change with the compiler versions, we need a more reliable
3637+ -- "likely" primitive to control branch predication.
3638+ case x > 0x7f of
3639+ False ->
3640+ return $ Skip $ YieldAndContinue
3641+ (Right $ unsafeChr (fromIntegral x))
3642+ (FreshPointDecodeInit st)
3643+ -- Using a separate state here generates a jump to a
3644+ -- separate code block in the core which seems to perform
3645+ -- slightly better for the non-ascii case.
3646+ True -> return $ Skip $ FreshPointDecodeFirst st x
3647+
3648+ -- XXX should we merge it with FreshPointDecodeInit1?
3649+ step' table _ (FreshPointDecodeFirst st x) = do
3650+ let (Tuple' sv cp) = decode0 table x
3651+ return $
3652+ case sv of
3653+ 12 ->
3654+ Skip $ YieldAndContinue (Left $ DecodeError 0 (fromIntegral x))
3655+ (FreshPointDecodeInit st)
3656+ 0 -> error "unreachable state"
3657+ _ -> Skip (FreshPointDecoding st sv cp)
3658+
3659+ -- We recover by trying the new byte x a starter of a new codepoint.
3660+ -- XXX need to use the same recovery in array decoding routine as well
3661+ step' table gst (FreshPointDecoding st statePtr codepointPtr) = do
3662+ r <- step (adaptState gst) st
3663+ case r of
3664+ Yield x s -> do
3665+ let (Tuple' sv cp) = decode1 table statePtr codepointPtr x
3666+ return $
3667+ case sv of
3668+ 0 -> Skip $ YieldAndContinue (Right $ unsafeChr cp)
3669+ (FreshPointDecodeInit s)
3670+ 12 ->
3671+ Skip $ YieldAndContinue (Left $ DecodeError statePtr codepointPtr)
3672+ (FreshPointDecodeInit1 s x)
3673+ _ -> Skip (FreshPointDecoding s sv cp)
3674+ Skip s -> return $ Skip (FreshPointDecoding s statePtr codepointPtr)
3675+ Stop -> return $ Skip $ YieldAndContinue (Left $ DecodeError statePtr codepointPtr) Done
3676+
3677+ step' _ _ (YieldAndContinue c s) = return $ Yield c s
3678+ step' _ _ Done = return Stop
3679+
3680+{-# INLINE_NORMAL decodeUtf8Either #-}
3681+decodeUtf8Either :: Monad m
3682+ => Stream m Word8 -> Stream m (Either DecodeError Char)
3683+decodeUtf8Either = resumeDecodeUtf8Either 0 0
3684+
35883685data FlattenState s a
3589− = OuterLoop s !(Maybe (DecoderState, CodePoint))
3686+ = OuterLoop s !(Maybe (DecodeState, CodePoint))
35903687 | InnerLoopDecodeInit s (ForeignPtr a) !(Ptr a) !(Ptr a)
35913688 | InnerLoopDecodeFirst s (ForeignPtr a) !(Ptr a) !(Ptr a) Word8
35923689 | InnerLoopDecoding s (ForeignPtr a) !(Ptr a) !(Ptr a)
3593− !DecoderState !CodePoint
3690+ !DecodeState !CodePoint
35943691 | YAndC !Char (FlattenState s a) -- These constructors can be
35953692 -- encoded in the FreshPoint
35963693 -- type, I prefer to keep these
streamly.cabal+25
@@ -949,3 +949,28 @@ executable WordClassifier
949949 , unordered-containers >= 0.2 && < 0.3
950950 else
951951 buildable: False
952+
953+executable WordCount
954+ import: exe-options
955+ main-is: WordCount.hs
956+ hs-source-dirs: examples
957+ if (flag(examples) || flag(examples-sdl)) && !impl(ghcjs)
958+ buildable: True
959+ build-Depends:
960+ streamly
961+ , base >= 4.8 && < 5
962+ , vector >= 0.12 && < 0.13
963+ else
964+ buildable: False
965+
966+executable CamelCase
967+ import: exe-options
968+ main-is: CamelCase.hs
969+ hs-source-dirs: examples
970+ if (flag(examples) || flag(examples-sdl)) && !impl(ghcjs)
971+ buildable: True
972+ build-Depends:
973+ streamly
974+ , base >= 4.8 && < 5
975+ else
976+ buildable: False