Haskell: Data.Text vs. Data.Text.Lazy性能比较

3

为了训练,我写了一个短小的Haskell程序来替代Perl脚本。该程序读取一个包含多行消息的日志文件,并将它们连接起来以产生每个消息一行的输出。我的测试输入文件有14000行,大小为1MB。使用Data.Text编写的版本运行时间为3.5秒,而使用Data.Text.Lazy编写的版本只需0.1秒(原始的perl脚本需要0.2秒)。我在其他帖子中发现,只有在处理大量数据时使用Data.Text.Lazy才有意义,我并没有期望会有这样的巨大差异。有人能解释一下原因或找出我的程序哪里出错了吗?

源代码的相关部分(两个版本之间唯一的区别是import 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

抛开问题不谈,这是一个十分适合使用 conduitpipes 的案例。 - Alec
getInput 标识符不在作用域内,它是否在程序的其他地方定义或从其他地方导入? - Michael Snoyman
getInput 在其他地方定义,根据命令行参数它返回标准输入(stdin)或打开的输入文件句柄,以及标准输出(stdout)或输出文件句柄。 - Ralli
2个回答

1
这似乎是一个数据结构问题,而不是懒惰问题。严格的 Text 本质上是一大块内存,而懒惰的 Text 本质上是严格的 Text(“块”)的链表。将懒惰的 Text 分成块的方式不应该是值含义的一部分(值只是通过连接所有块获得的文本)。但它可能会对操作的成本产生很大影响。
您有一堆形式为short <> accu的操作,其中accu随着输出大小而增长。如果这些是严格的Text,则此连接必须将shortaccu的整个内容复制到新的严格Text值中。总运行时间必然是二次的。如果它们是惰性的Text,那么<>还有另一个选择:它可以将short的块列表前置到accu的块列表中。这根本不需要触及accu,但即使如此,构成accu的块链表的脊柱可能比accu的整个文本内容少得多,这取决于块大小。这就是为什么您的惰性Text版本要快得多的原因。

看起来您可以以以下形式编写程序

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

这就留下了一个问题,如何将它们连接起来传递给库函数T.unlines,在这种情况下,无论您使用严格的Text还是惰性的Text,都可以期望其效率高。


在按照建议重新编写程序后,两个版本的运行时间相同。 - Ralli

0
懒加载和正常的 Data.Text 的区别在于是否将整个文件读入内存。
actLog <- T.hGetContents inp 

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


我理解这一点,但程序在两种情况下都会读取和处理完整的文件。我认为一次性读取一个1MB的文件应该不会比逐行读取慢35倍。 - Ralli
内存占用也小了很多,但我同意这只是速度提升的一部分解释。你可以通过将“let newLog = processLog actLog”更改为“newLog <- processLog actLog”来强制使用内存并消除一些惰性,看看是否能够以与使用非惰性Data.Text相同的顺序提高速度。 - prof1990
"newLog <- processLog actLog" 无法编译。 - Ralli
我的错,忘记在单子中包装它了。"newLog <- return $ processLog actLog" 应该可以编译。但不确定它是否实际强制执行严格性。 - prof1990
是的,这确实可以编译,但运行时仍然相同。 - Ralli

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接