Haskell: Data.Text vs. Data.Text.Lazy Performance



为了进行培训,我编写了一个简短的Haskell程序来代替Perl脚本。该程序读取一个包含多行消息的日志文件,并简单地将它们连接起来,为每条消息生成一行。我的测试输入文件有14000行,大小为1MB。使用Data.Text的版本的运行时间为3.5秒,使用Data.Text.Lazy的版本只有0.1秒(原始perl脚本需要0.2秒)。我在其他帖子中发现,使用Data.Text.Lazy只对大量数据有意义,没想到会有这样的区别。有人能解释一下我的程序为什么或出了什么问题吗?

源的相关部分(两个版本之间的唯一区别是导入Data.Text*):

{-# LANGUAGE OverloadedStrings #-}
module Main where
import Data.Char (isDigit)
import Data.Monoid ((<>))
import qualified Data.Text.Lazy    as T
import qualified Data.Text.Lazy.IO as T
import System.Environment (getArgs, getProgName)
import System.IO (openFile, stdin, stdout, Handle,
IOMode(ReadMode,WriteMode))
main :: IO ()
main = do
(inp,out) <- getInput
actLog <- T.hGetContents inp
let newLog = processLog actLog
T.hPutStr out newLog
processLog :: T.Text -> T.Text
processLog = foldr joinLines "" . T.lines
joinLines :: T.Text -> T.Text -> T.Text
joinLines elem accu
| T.length elem == 0    = accu                     -- Blank Line
| T.head elem   == ' '  = textElem <> accu         -- Continuation
| isDigit (T.head elem) = "n" <> textElem <> accu -- Start
| otherwise             = accu                     -- Garbage
where textElem = T.strip elem

这看起来像是一个数据结构问题,而不是懒惰问题。严格的Text本质上是一个大的内存块,而懒惰的Text本质上是严格Text("块")的链表。懒惰的Text被分割成块的方式不应该是值意义的一部分(它只是通过连接所有块获得的文本)。但这可能会对运营成本产生重大影响。

您有一组形式为short <> accu的操作,其中accu随着输出的大小而增长。如果这些是严格的Text,则这种级联必须将shortaccu的全部内容复制到新的严格的Text值中。总运行时间必然是二次的。如果它们是懒惰的Texts,那么<>有另一个选项:它可以将short的块列表预先添加到accu的块列表中。这根本不需要触摸accu,但即使触摸了,根据块的大小,组成accu的块的链表的主干可能比accu的整个文本内容要处理的数据少得多。这就是为什么你的懒惰Text版本要快得多。

看起来你可以用的形式编写程序

processLog = T.unlines . processLines . T.lines
processLines :: [T.Text] -> [T.Text]
processLines = ...

这就留下了如何连接到库函数T.unlines的问题,在这种情况下,无论您使用严格的Text还是懒惰的Text,都可以期望它是高效的。

延迟数据和普通数据之间的区别在于整个文件是否在中读取到内存中

actLog <- T.hGetContents inp 

处理时,惰性Haskell只读取生成输出直接需要的行。因此,它不再读取整个文件,然后根据需要进行处理和写入,而是可以根据需要进行读取、写入和处理,从而消除了在内存中读取整个文件的等待。

最新更新