-- | -- Module : Streamly.Benchmark.FileSystem.Handle -- Copyright : (c) 2019 Composewell Technologies -- License : BSD-3-Clause -- Maintainer : streamly@composewell.com -- Stability : experimental -- Portability : GHC {-# LANGUAGE CPP #-} {-# LANGUAGE ScopedTypeVariables #-} #ifdef __HADDOCK_VERSION__ #undef INSPECTION #endif #ifdef INSPECTION {-# LANGUAGE TemplateHaskell #-} {-# OPTIONS_GHC -fplugin Test.Inspection.Plugin #-} #endif module Handle.Read (allBenchmarks) where import Data.Functor.Identity (runIdentity) import Data.Word (Word8) import GHC.Magic (inline) #if __GLASGOW_HASKELL__ >= 802 import GHC.Magic (noinline) #else #define noinline #endif import System.IO (Handle) import qualified Streamly.FileSystem.Handle as FH import qualified Streamly.Internal.Data.Array.Storable.Foreign as A import qualified Streamly.Internal.Data.Array.Storable.Foreign.Types as AT import qualified Streamly.Internal.Data.Fold as FL import qualified Streamly.Internal.Data.Parser as PR import qualified Streamly.Internal.Data.Stream.IsStream as IP import qualified Streamly.Internal.FileSystem.Handle as IFH import qualified Streamly.Internal.Memory.ArrayStream as AS import qualified Streamly.Internal.Unicode.Stream as IUS import qualified Streamly.Prelude as S import qualified Streamly.Unicode.Stream as SS import Gauge hiding (env) import Prelude hiding (last, length) import Streamly.Benchmark.Common.Handle #ifdef INSPECTION import Streamly.Internal.Data.Stream.StreamD.Type (Step(..), GroupState) import qualified Streamly.Internal.Data.Stream.StreamD as D import qualified Streamly.Internal.Data.Unfold as IUF import Test.Inspection #endif ------------------------------------------------------------------------------- -- read chunked using toChunks ------------------------------------------------------------------------------- -- | Get the last byte from a file bytestream. toChunksLast :: Handle -> IO (Maybe Word8) toChunksLast inh = do let s = IFH.toChunks inh larr <- S.last s return $ case larr of Nothing -> Nothing Just arr -> A.readIndex arr (A.length arr - 1) #ifdef INSPECTION inspect $ hasNoTypeClasses 'toChunksLast inspect $ 'toChunksLast `hasNoType` ''Step #endif -- | Count the number of bytes in a file. toChunksSumLengths :: Handle -> IO Int toChunksSumLengths inh = let s = IFH.toChunks inh in S.sum (S.map A.length s) #ifdef INSPECTION inspect $ hasNoTypeClasses 'toChunksSumLengths inspect $ 'toChunksSumLengths `hasNoType` ''Step #endif -- | Count the number of lines in a file. toChunksSplitOnSuffix :: Handle -> IO Int toChunksSplitOnSuffix = S.length . AS.splitOnSuffix 10 . IFH.toChunks #ifdef INSPECTION inspect $ hasNoTypeClasses 'toChunksSplitOnSuffix inspect $ 'toChunksSplitOnSuffix `hasNoType` ''Step #endif -- XXX use a word splitting combinator instead of splitOn and test it. -- | Count the number of words in a file. toChunksSplitOn :: Handle -> IO Int toChunksSplitOn = S.length . AS.splitOn 32 . IFH.toChunks #ifdef INSPECTION inspect $ hasNoTypeClasses 'toChunksSplitOn inspect $ 'toChunksSplitOn `hasNoType` ''Step #endif -- | Sum the bytes in a file. toChunksCountBytes :: Handle -> IO Word8 toChunksCountBytes inh = do let foldlArr' f z = runIdentity . S.foldl' f z . A.toStream let s = IFH.toChunks inh S.foldl' (\acc arr -> acc + foldlArr' (+) 0 arr) 0 s #ifdef INSPECTION inspect $ hasNoTypeClasses 'toChunksCountBytes inspect $ 'toChunksCountBytes `hasNoType` ''Step #endif toChunksDecodeUtf8Arrays :: Handle -> IO () toChunksDecodeUtf8Arrays = S.drain . IUS.decodeUtf8Arrays . IFH.toChunks #ifdef INSPECTION inspect $ hasNoTypeClasses 'toChunksDecodeUtf8Arrays -- inspect $ 'toChunksDecodeUtf8ArraysLenient `hasNoType` ''Step #endif o_1_space_read_chunked :: BenchEnv -> [Benchmark] o_1_space_read_chunked env = -- read using toChunks instead of read [ bgroup "reduce/toChunks" [ mkBench "S.last" env $ \inH _ -> toChunksLast inH -- Note: this cannot be fairly compared with GNU wc -c or wc -m as -- wc uses lseek to just determine the file size rather than reading -- and counting characters. , mkBench "S.sum . S.map A.length" env $ \inH _ -> toChunksSumLengths inH , mkBench "AS.splitOnSuffix" env $ \inH _ -> toChunksSplitOnSuffix inH , mkBench "AS.splitOn" env $ \inH _ -> toChunksSplitOn inH , mkBench "countBytes" env $ \inH _ -> toChunksCountBytes inH , mkBenchSmall "US.decodeUtf8Arrays" env $ \inH _ -> toChunksDecodeUtf8Arrays inH ] ] -- TBD reading with unfold ------------------------------------------------------------------------------- -- unfold read ------------------------------------------------------------------------------- -- | Get the last byte from a file bytestream. readLast :: Handle -> IO (Maybe Word8) readLast = S.last . S.unfold FH.read #ifdef INSPECTION inspect $ hasNoTypeClasses 'readLast inspect $ 'readLast `hasNoType` ''Step -- S.unfold inspect $ 'readLast `hasNoType` ''IUF.ConcatState -- FH.read/UF.concat inspect $ 'readLast `hasNoType` ''A.ReadUState -- FH.read/A.read #endif -- assert that flattenArrays constructors are not present -- | Count the number of bytes in a file. readCountBytes :: Handle -> IO Int readCountBytes = S.length . S.unfold FH.read #ifdef INSPECTION inspect $ hasNoTypeClasses 'readCountBytes inspect $ 'readCountBytes `hasNoType` ''Step -- S.unfold inspect $ 'readCountBytes `hasNoType` ''IUF.ConcatState -- FH.read/UF.concat inspect $ 'readCountBytes `hasNoType` ''A.ReadUState -- FH.read/A.read #endif -- | Count the number of lines in a file. readCountLines :: Handle -> IO Int readCountLines = S.length . IUS.lines FL.drain . SS.decodeLatin1 . S.unfold FH.read #ifdef INSPECTION inspect $ hasNoTypeClasses 'readCountLines inspect $ 'readCountLines `hasNoType` ''Step inspect $ 'readCountLines `hasNoType` ''IUF.ConcatState -- FH.read/UF.concat inspect $ 'readCountLines `hasNoType` ''A.ReadUState -- FH.read/A.read #endif -- | Count the number of words in a file. readCountWords :: Handle -> IO Int readCountWords = S.length . IUS.words FL.drain . SS.decodeLatin1 . S.unfold FH.read #ifdef INSPECTION inspect $ hasNoTypeClasses 'readCountWords -- inspect $ 'readCountWords `hasNoType` ''Step #endif -- | Sum the bytes in a file. readSumBytes :: Handle -> IO Word8 readSumBytes = S.sum . S.unfold FH.read #ifdef INSPECTION inspect $ hasNoTypeClasses 'readSumBytes inspect $ 'readSumBytes `hasNoType` ''Step inspect $ 'readSumBytes `hasNoType` ''IUF.ConcatState -- FH.read/UF.concat inspect $ 'readSumBytes `hasNoType` ''A.ReadUState -- FH.read/A.read #endif -- XXX When we mark this with INLINE and we have two benchmarks using S.drain -- in one benchmark group then somehow GHC ends up delaying the inlining of -- readDrain. Since S.drain has an INLINE[2] for proper rule firing, that does -- not work well because of delyaed inlining and the code does not fuse. We -- need some way of propagating the inline phase information up so that we can -- expedite inlining of the callers too automatically. The minimal example for -- the problem can be created by using just two benchmarks in a bench group -- both using "readDrain". Either GHC should be fixed or we can use -- fusion-plugin to propagate INLINE phase information such that this problem -- does not occur. readDrain :: Handle -> IO () readDrain inh = S.drain $ S.unfold FH.read inh -- XXX investigate why we need an INLINE in this case (GHC) {-# INLINE readDecodeLatin1 #-} readDecodeLatin1 :: Handle -> IO () readDecodeLatin1 inh = S.drain $ SS.decodeLatin1 $ S.unfold FH.read inh readDecodeUtf8 :: Handle -> IO () readDecodeUtf8 inh = S.drain $ SS.decodeUtf8 $ S.unfold FH.read inh #ifdef INSPECTION inspect $ hasNoTypeClasses 'readDecodeUtf8 -- inspect $ 'readDecodeUtf8Lax `hasNoType` ''Step #endif o_1_space_reduce_read :: BenchEnv -> [Benchmark] o_1_space_reduce_read env = [ bgroup "reduce/read" [ -- read raw bytes without any decoding mkBench "S.drain" env $ \inh _ -> readDrain inh , mkBench "S.last" env $ \inh _ -> readLast inh , mkBench "S.sum" env $ \inh _ -> readSumBytes inh -- read with Latin1 decoding , mkBench "SS.decodeLatin1" env $ \inh _ -> readDecodeLatin1 inh , mkBench "S.length" env $ \inh _ -> readCountBytes inh , mkBench "US.lines . SS.decodeLatin1" env $ \inh _ -> readCountLines inh , mkBenchSmall "US.words . SS.decodeLatin1" env $ \inh _ -> readCountWords inh -- read with utf8 decoding , mkBenchSmall "SS.decodeUtf8" env $ \inh _ -> readDecodeUtf8 inh ] ] ------------------------------------------------------------------------------- -- stream toBytes ------------------------------------------------------------------------------- -- | Count the number of lines in a file. toChunksConcatUnfoldCountLines :: Handle -> IO Int toChunksConcatUnfoldCountLines inh = S.length $ IUS.lines FL.drain $ SS.decodeLatin1 -- XXX replace with toBytes $ S.concatUnfold A.read (IFH.toChunks inh) #ifdef INSPECTION inspect $ hasNoTypeClasses 'toChunksConcatUnfoldCountLines inspect $ 'toChunksConcatUnfoldCountLines `hasNoType` ''Step inspect $ 'toChunksConcatUnfoldCountLines `hasNoType` ''D.ConcatMapUState #endif o_1_space_reduce_toBytes :: BenchEnv -> [Benchmark] o_1_space_reduce_toBytes env = [ bgroup "reduce/toBytes" [ mkBench "US.lines . SS.decodeLatin1" env $ \inh _ -> toChunksConcatUnfoldCountLines inh ] ] ------------------------------------------------------------------------------- -- reduce after grouping in chunks ------------------------------------------------------------------------------- chunksOfSum :: Int -> Handle -> IO Int chunksOfSum n inh = S.length $ S.chunksOf n FL.sum (S.unfold FH.read inh) foldManyChunksOfSum :: Int -> Handle -> IO Int foldManyChunksOfSum n inh = S.length $ IP.foldMany (FL.ltake n FL.sum) (S.unfold FH.read inh) parseManyChunksOfSum :: Int -> Handle -> IO Int parseManyChunksOfSum n inh = S.length $ IP.parseMany (PR.fromFold $ FL.ltake n FL.sum) (S.unfold FH.read inh) -- XXX investigate why we need an INLINE in this case (GHC) -- Even though allocations remain the same in both cases inlining improves time -- by 4x. -- | Slice in chunks of size n and get the count of chunks. {-# INLINE chunksOf #-} chunksOf :: Int -> Handle -> IO Int chunksOf n inh = -- writeNUnsafe gives 2.5x boost here over writeN. -- XXX replace with S.arraysOf S.length $ S.chunksOf n (AT.writeNUnsafe n) (S.unfold FH.read inh) #ifdef INSPECTION inspect $ hasNoTypeClasses 'chunksOf inspect $ 'chunksOf `hasNoType` ''Step inspect $ 'chunksOf `hasNoType` ''GroupState inspect $ 'chunksOf `hasNoType` ''AT.ArrayUnsafe -- AT.writeNUnsafe inspect $ 'chunksOf `hasNoType` ''IUF.ConcatState -- FH.read/UF.concat inspect $ 'chunksOf `hasNoType` ''A.ReadUState -- FH.read/A.read #endif o_1_space_reduce_read_grouped :: BenchEnv -> [Benchmark] o_1_space_reduce_read_grouped env = [ bgroup "reduce/read/chunks" [ mkBench ("S.chunksOf " ++ show (bigSize env) ++ " FL.sum") env $ \inh _ -> chunksOfSum (bigSize env) inh , mkBench "S.chunksOf 1 FL.sum" env $ \inh _ -> chunksOfSum 1 inh -- XXX investigate why we need inline/noinline in these cases (GHC) -- Chunk using parsers , mkBenchSmall ("S.foldMany (FL.take " ++ show (bigSize env) ++ " FL.sum)") env $ \inh _ -> noinline foldManyChunksOfSum (bigSize env) inh , mkBench "S.foldMany (FL.take 1 FL.sum)" env $ \inh _ -> inline foldManyChunksOfSum 1 inh , mkBenchSmall ("S.parseMany (FL.take " ++ show (bigSize env) ++ " FL.sum)") env $ \inh _ -> noinline parseManyChunksOfSum (bigSize env) inh , mkBench "S.parseMany (FL.take 1 FL.sum)" env $ \inh _ -> inline parseManyChunksOfSum 1 inh -- folding chunks to arrays , mkBenchSmall "S.arraysOf 1" env $ \inh _ -> chunksOf 1 inh , mkBench "S.arraysOf 10" env $ \inh _ -> chunksOf 10 inh , mkBench "S.arraysOf 1000" env $ \inh _ -> chunksOf 1000 inh ] ] allBenchmarks :: BenchEnv -> [Benchmark] allBenchmarks env = Prelude.concat [ o_1_space_read_chunked env , o_1_space_reduce_read env , o_1_space_reduce_toBytes env , o_1_space_reduce_read_grouped env ]