我使用haskell进行基于行的数据处理,即可以应用sed
,awk
和类似工具的任务。作为一个简单的例子,让我们将000
添加到标准输入的每一行。
我有三种替代方法来完成任务:
ByteString
s ByteString
处理。 example.hs
:
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE FlexibleContexts #-}
import ClassyPrelude.Conduit
import qualified Data.ByteString.Char8 as B8
import qualified Data.ByteString.Lazy.Char8 as BL8
import qualified Data.Conduit.Binary as CB
main = do
[arg] <- getArgs
case arg of
"lazy" -> BL8.getContents >>= BL8.putStr . BL8.unlines . map ("000" ++) . BL8.lines
"lines" -> runConduitRes $ stdinC .| CB.lines .|
mapC ("000" ++) .| mapC (`snoc` 10) .| stdoutC
"chunks" -> runConduitRes $ stdinC .| lineChunksC .|
mapC (B8.unlines . (map ("000" ++)) . B8.lines) .| stdoutC
lineChunksC :: Monad m => Conduit ByteString m ByteString
lineChunksC = await >>= maybe (return ()) go
where
go acc = if
| Just (_, 10) <- unsnoc acc -> yield acc >> lineChunksC
| otherwise -> await >>= maybe (yield acc) (go' . breakAfterEOL)
where
go' (this, next) = let acc' = acc ++ this in if null next then go acc' else yield acc' >> go next
breakAfterEOL :: ByteString -> (ByteString, ByteString)
breakAfterEOL = uncurry (\x -> maybe (x, "") (first (snoc x)) . uncons) . break (== 10)
$ stack ghc --package={classy-prelude-conduit,conduit-extra} -- -O2 example.hs -o example $ for cmd in lazy lines chunks; do echo $cmd; time -p seq 10000000 | ./example $cmd > /dev/null; echo; done lazy real 2.99 user 3.06 sys 0.07 lines real 3.30 user 3.36 sys 0.06 chunks real 1.83 user 1.95 sys 0.06
(结果在多次运行中保持一致,并且也适用于包含多个数字的行。)
所以chunks
比lines
快1.6倍,比lazy
快一点。这意味着管道可以比普通的字节串更快,但是当你将块分成短线时,管道管道的开销太大了。
我对chunks
方法的不同之处在于它混合了管道和纯净的世界,并且使得它更难用于更复杂的任务。
问题是,我是否错过了一个简单而优雅的解决方案,这使我能够以与lines
方法相同的方式编写有效的代码?
EDIT1:Per @ Michael的建议我在mapC
解决方案中将两个mapC (("000" ++). (
加入了一个10))
snoc lines
,以制作多个管道({{ 1}}).|
和lines
之间相同。这使它表现得更好(从3.3秒降至2.8秒),但仍然明显慢于chunks
。
此外,我尝试了Michael在评论中提出的较早的chunks
,并且它还提高了性能,提高了约0.1秒。
EDIT2:已修复Conduit.Binary.lines
,因此适用于非常小的块,例如
lineChunksC
答案 0 :(得分:3)
我的猜测是,对于“行”,mapC ("000" ++) .| mapC (`snoc` 10)
部分正在做很多工作。
将几个严格的ByteStrings
连接到另一个严格的ByteString
是很昂贵的。将它们连接成一个懒惰的ByteString
往往更有效率。
为了避免这种成本,您可以将每个部分单独下游作为严格ByteString
(但请注意,我们不再讨论“行”)。
或者,将每个转换的行生成为下游的惰性ByteString
。
问题是,我是否错过了一个简单而优雅的解决方案 允许我以与行相同的方式编写有效的代码 接近?
某些流式库有一个有趣的特性:您可以在流中划分行并对其进行操作,而无需在任何时候在内存中实现整行。
我在这里使用streaming和streaming-bytestring软件包,因为我对它们比较熟悉。
在 streaming-bytestring 的模块Data.ByteString.Streaming.Char8
中,我们有lines
函数:
lines :: Monad m => ByteString m r -> Stream (ByteString m) m r
行将ByteString转换为ByteStrings的连接流 以换行符分隔。生成的字符串不包含 换行。这是真正的流线,只会打破 块,因此永远不会增加内存的使用。
它的要点是ByteString m r
已经流媒体类型!因此,此版本的lines
将流转换为“流的流”。我们只能通过耗尽“当前流”(当前行)来到达“下一个流”(下一行)。
您的“行”示例可写为:
{-# language OverloadedStrings #-}
module Main where
import Control.Applicative ((*>))
import Streaming
import qualified Streaming.Prelude as S
import qualified Data.ByteString.Streaming.Char8 as Q
main :: IO ()
main = Q.stdout
. Q.unlines
. S.maps (\line -> "000" *> line)
. Q.lines
$ Q.stdin