提高基于线路的管道性能的方法

时间:2016-10-29 13:46:28

标签: haskell io conduit

我使用haskell进行基于行的数据处理,即可以应用sedawk和类似工具的任务。作为一个简单的例子,让我们将000添加到标准输入的每一行。

我有三种替代方法来完成任务:

  1. lazy IO with lazy ByteString s
  2. 基于行的管道。
  3. 基于块的管道,内部采用纯粹严格的ByteString处理。
  4. 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
    

    (结果在多次运行中保持一致,并且也适用于包含多个数字的行。)

    所以chunkslines快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

1 个答案:

答案 0 :(得分:3)

我的猜测是,对于“行”,mapC ("000" ++) .| mapC (`snoc` 10)部分正在做很多工作。

将几个严格的ByteStrings连接到另一个严格的ByteString是很昂贵的。将它们连接成一个懒惰的ByteString往往更有效率。

为了避免这种成本,您可以将每个部分单独下游作为严格ByteString(但请注意,我们不再讨论“行”)。

或者,将每个转换的行生成为下游的惰性ByteString

  

问题是,我是否错过了一个简单而优雅的解决方案   允许我以与行相同的方式编写有效的代码   接近?

某些流式库有一个有趣的特性:您可以在流中划分行并对其进行操作,而无需在任何时候在内存中实现整行。

我在这里使用streamingstreaming-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