【问题标题】:How to process two long lines with constant memory?如何处理具有恒定内存的两条长线?
【发布时间】:2016-01-22 07:23:25
【问题描述】:

输入文件由两行组成,每行包含许多数字

1 2 3 4... 
5 6 7 8... 

我想处理每一行的数据,像这样:

doSomething :: [Int] -> [Int] -> Int
doSomething [] y = 0 -- stop execution. I'm concerted in memory behavior only.
doSomething (x:xs) y = doSomething xs y
main = do
    inputdata <- getContents
    let (x:xs) = lines inputdata
        firstLine = map (read ::String->Int) $ words $ x
        secondLine = map (read ::String->Int) $ words $ head xs
    print $ doSomething firstLine secondLine

当我运行这个程序时,堆分析显示如下:

如果我不使用 secondLine (xs),那么这个程序会以恒定内存运行。 列表firstLine 的每个条目都会被 GC 处理然后丢弃。

  1. 为什么消耗的内存这么大? 我看到分配的内存量约为 100MB,但实际输入数据大小为 5MB。

  2. head xs 是否强制将整个第一行读入内存, 甚至secondLine 根本没有使用?这是主要原因吗 堆分析图的内存增加?

  3. 如何处理具有恒定内存的两行,例如后面的​​执行图?

  4. 如果 3) 的答案取决于处理顺序,如何先处理第二行,然后处理第一行?

【问题讨论】:

  • 请注意,Haskell 的String 非常消耗内存,因为每个字符占用(至少)4 个字节。因此,如果您有一个 5MB 的文本文件,将其全部读入内存将使用 20+MB 的 RAM。然后,您将创建相同长度的Int 列表,这至少应使内存增加一倍。如果它不懒惰,对doSomething 的调用将再次翻倍,轻松达到 80+MB。使用ByteString 可能会显着减少这种情况(并使读写效率更高)。
  • @cwyang 您的评论与上面的代码冲突。如果doSomething 强制y(secondLine) 被评估,这将导致lines 继续直到找到换行符,同时记住它之前的内容,因为它将被放入firstLine。那么:正如您的评论所暗示的那样,您实际上是否在第一行之前访问了第二行?
  • 计算列表节点和内部元数据的开销,在 64 位架构 String 上每个字符占用 24 个字节。如果您的输入本质上是 ASCII,那么空间使用量将增加 24 倍。 String 空间效率极低(而且通常效率低下)。因此TextByteString
  • 作为 Q3 在实践中的答案,您应该研究流式 IO 库。目前最流行的一些库是pipesconduitio-streams。与尝试使用 getContents 相比,其中任何一个都将更快、更不脆弱且更具功能性(例如支持压缩 IO)。
  • 当使用-O2 编译时,您的程序实际上是使用恒定内存运行的。如果不使用firstLine,程序也会以恒定内存运行。

标签: performance haskell lazy-evaluation


【解决方案1】:

Q1) 为什么消耗的内存这么大?我看到分配的内存量约为 100MB,但实际输入数据大小为 5MB。

Haskell 中的String[Char] 的类型别名,因此在实际字节中,编译器还必须为内存,结果> 10 倍于 C 样式字符串的内存使用量。更糟糕的是,文本会在内存中多次存储。

Q3) 如何处理具有恒定内存的两行,例如后面的​​执行图?
Q4) 如果 Q3) 的答案取决于处理顺序,如何先处理第二行,然后处理第一行?

不,您不能先处理第二行,因为 lines 函数必须评估第一行中的每个字节才能命中 '\n' 换行符。

Q2) head xs 是否强制将整个第一行读入内存,甚至根本不使用第二行?这是堆分析图内存增加的主要原因吗?

阻止第一行被 GC 的不是 head。如果调整doSomething 的类型签名并直接将xs 传递给它,仍然会发生空间泄漏。关键是(未优化)编译器不会知道在 doSomething 最终达到第一个模式之前没有使用 secondLine,因此程序保留对 xs 的引用。顺便说一句,如果使用 -O2 编译,您的程序将使用常量内存运行。

导致你程序空间泄漏的原因主要是这行:

let (x:xs) = lines inputdata

xxs 被丢弃时,这个 let 子句将内联到转储的核心中。只有当它们都被稍后引用时,Core 才会出现奇怪的行为:它构造一个元组,通过模式匹配来破坏它,然后再次将这两个部分构造成一个元组,因此通过保持对 secondLine 的引用,程序实际上保持了一个引用到一个元组(x, xs),所以第一行永远不会被GCed。

带有secondLine 的核心已被注释掉:

Rec {
doSomething_rjH
doSomething_rjH =
  \ ds_d1lv y_alG ->
    case ds_d1lv of _ {
      [] -> I# 0;
      : x_alH xs_alI -> doSomething_rjH xs_alI y_alG
    }
end Rec }

main
main =
  >>=
    $fMonadIO
    getContents
    (\ inputdata_app ->
       print
         $fShowInt
         (doSomething_rjH
            (map
               (read $fReadInt)
               (words
                  (case lines inputdata_app of _ {
                     [] -> case irrefutPatError "a.hs:6:9-32|(x : xs)"# of wild1_00 { };
                     : x_auf xs_aug -> x_auf
                   })))
            ([])))

main
main = runMainIO main

有空间泄漏的核心:

Rec {
doSomething_rjH
doSomething_rjH =
  \ ds_d1ol y_alG ->
    case ds_d1ol of _ {
      [] -> I# 0;
      : x_alH xs_alI -> doSomething_rjH xs_alI y_alG
    }
end Rec }

main
main =
  >>=
    $fMonadIO
    getContents
    (\ inputdata_app ->
       -- *** Construct ***
       let {
         ds_d1op
         ds_d1op =
           case lines inputdata_app of _ {
             [] -> irrefutPatError "a.hs:6:9-30|x : xs"#;
             : x_awM xs_awN -> (x_awM, xs_awN)
           } } in
       -- *** Destruct ***
       let {
         xs_awN
         xs_awN = case ds_d1op of _ { (x_awM, xs1_XwZ) -> xs1_XwZ } } in
       let {
         x_awM
         x_awM = case ds_d1op of _ { (x1_XwZ, xs1_XwU) -> x1_XwZ } } in
       -- *** Construct ***
       let {
         ds1_d1oq
         ds1_d1oq = (x_awM, xs_awN) } in
       print
         $fShowInt
         -- *** Destruct ***
         (doSomething_rjH
            (map
               (read $fReadInt)
               (words (case ds1_d1oq of _ { (x1_Xx1, xs1_Xx3) -> x1_Xx1 })))
            (map
               (read $fReadInt)
               (words
                  (head (case ds1_d1oq of _ { (x1_Xx1, xs1_Xx3) -> xs1_Xx3 }))))))

main
main = runMainIO main

case .. of 子句替换let 子句将修复空间泄漏:

doSomething :: [Int] -> [Int] -> Int
doSomething [] _ = 0 -- stop execution. I'm concerted in memory behavior only.
doSomething (_:xs) y = doSomething xs y

main :: IO ()
main = do
  inputdata <- getContents
  case lines inputdata of
    x:xs -> do
      let
        firstLine = map (read ::String->Int) $ words x
        secondLine = map (read ::String->Int) $ words $ head xs
      print $ doSomething firstLine secondLine

转储的核心。这次没有发生“先构建后销毁”的模式:

Rec {
doSomething_rjG
doSomething_rjG =
  \ ds_d1o6 ds1_d1o7 ->
    case ds_d1o6 of _ {
      [] -> I# 0;
      : ds2_d1o8 xs_alG -> doSomething_rjG xs_alG ds1_d1o7
    }
end Rec }

main
main =
  >>=
    $fMonadIO
    getContents
    (\ inputdata_apn ->
       case lines inputdata_apn of _ {
         [] -> patError "a.hs:(8,3)-(13,46)|case"#;
         : x_asI xs_asJ ->
           print
             $fShowInt
             (doSomething_rjG
                (map (read $fReadInt) (words x_asI))
                (map (read $fReadInt) (words (head xs_asJ))))
       })

main
main = runMainIO main

【讨论】:

  • 谢谢 :-) 我知道了,但是这个:It's not the head that prevents the first line from being GCed. The point is the (not optimizing) compiler won't know that secondLine isn't used before doSomething finally hits the first pattern, so the program keeps a reference to xs . GC 是否可以在遍历 x 时丢弃已处理的一个,即使它包含对 xs 的引用?因为遍历x 发生在点击xs 之前。如果 GC 持有 x 的另一个引用,则 GC 无法处置 x。如果首先遍历“xs”,GC 也无法处理“x”。但在这种情况下不是。
  • @cwyang 是的,您的论点完全有道理,应该是默认行为,并且是 ghc -O2 的默认行为(尽管我无法确认内存使用图,因为核心转储了ghc -O2 几乎不可读)但简单的ghc -O0 是......我猜只是没有那么急切地修复这种空间泄漏。我对您的程序进行了小幅调整,现在它在恒定内存中运行,您可以看到变化是多么微小,而它却能奏效。
猜你喜欢
  • 1970-01-01
  • 2014-04-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-07-08
  • 2020-03-05
相关资源
最近更新 更多