什么是空间泄漏?

时间:2017-09-01 20:43:39

标签: haskell memory-leaks

我发现了空间泄漏的haskell wiki页面,它声称列出了现实泄漏的例子,但它并没有。它并没有真正说明空间泄漏是什么;它只是链接到内存泄漏的页面。

空间泄漏是什么?

2 个答案:

答案 0 :(得分:17)

正如在@ Rasko的回答中所指出的,空间泄漏是指程序或特定计算使用比计算所需和/或程序员预期更多(通常更多)的内存的情况。 / p>

Haskell程序往往特别容易受到空间泄漏的影响,主要是因为懒惰的评估模型(有时因IO与此模型的交互方式而复杂化)以及语言的高度抽象性,这使得程序员难以确切地确定可能如何执行特定计算。

考虑一个具体的例子是有帮助的。这个Haskell计划:

main = print $ sum [1..1000000000]

是对前十亿个整数求和的惯用方法。使用-O2编译,它在常量内存中运行几秒钟(几兆字节,基本上是运行时开销)。

现在,任何程序员都希望程序能够在不占用内存的情况下总计第一个十亿个整数运行,但实际上这个Haskell版本的表现确实有点令人惊讶。毕竟,从字面上看,它在总结之前构造了十亿个整数的列表,因此它应该至少需要几千兆字节(仅用于存储十亿个整数,更不用说Haskell链表的开销)。

但是,延迟评估确保列表仅生成,因为它需要 - 同样重要的是 - 编译器执行的优化确保将列表元素添加到累积中总之,程序认识到它们不再需要并且允许它们被垃圾收集而不是保持它们直到计算结束。因此,在计算过程中的任何时候,只有一个滑动的窗口"进入列表中间需要保存在内存中 - 之前的元素已被丢弃,后来的元素尚未被懒惰地计算。 (事实上​​,优化比这更进一步:甚至没有构建列表,但这对程序员来说并不明显。)

Soooo ...... Haskell程序员习惯了这样的想法,即围绕巨型(甚至无限)数据结构进行折腾将只会工作"计算自动仅使用他们需要的内存。

但是,对程序进行了一些细微的改动,比如打印列表的长度作为我们所做的所有艰苦工作的证明:

main = let vals = [1..1000000000]
       in print (sum vals, length vals)

突然导致空间使用量爆炸到几十千兆字节(或者在我的笔记本电脑的情况下,大约13Gigs,然后开始绝望地交换并杀死它)。

这是空间泄漏。计算这个列表的总和和长度显然是可以使用"滑动窗口在恒定空间中完成的事情"查看列表,但上面的程序使用的内存比需要的多得多。事实证明,原因是,一旦列表被赋予了在两个地方使用的名称vals,编译器就不再允许使用"使用"要立即丢弃的元素。如果首先评估sum vals,则会懒惰地生成并汇总列表,但是整个巨型列表将保持不变,直到可以评估length vals

作为一个更实际的例子,您可以编写一个简单的程序来计算文件中的单词和字符:

main = do txt <- getContents
          print (length txt, length (words txt))

这适用于高达几兆字节的小型测试文件,但它在10meg文件上明显迟缓,如果你尝试在100meg文件上运行它,它会慢慢但肯定会开始吞噬所有可用内存。同样,问题在于 - 即使文件内容被懒惰地读入txt - 因为txt被使用了两次,所以整个内容作为Haskell String类型被读入内存(例如,length txt被评估时,(大块文本的内存效率低的表示),并且在计算length (words txt)之前,不能释放任何内存。

请注意:

main = do txt <- getContents
          print $ length txt

main = do txt <- getContents
          print $ length (words txt)

即使在大文件上也能在恒定的空间内快速运行。

作为旁注,修复上述空间泄漏通常涉及重写计算,以便通过内容一次性计算字符和单词,因此编译器可以确定已经处理的文件的内容不需要在内存中保留,直到计算结束。一种可能的解决方案是:

{-# LANGUAGE BangPatterns #-}

import Data.List
import Data.Char

charsWords :: String -> (Int, Int)
charsWords str = let (_, chrs, wrds) = foldl' step (False, 0, 0) str
                 in (chrs, wrds)
  where step (inWord, cs, ws) c =
          let !cs' = succ cs
              !ws' = if not inWord && inWord' then succ ws else ws
              !inWord' = not (isSpace c)
          in (inWord', cs', ws')

main = do txt <- getContents
          print $ charsWords txt

此解决方案的复杂性(使用bang(!)模式和显式折叠而不是lengthwords)说明了空间泄漏的严重程度,特别是对于新的Haskell程序员。使用foldl'代替foldl而不是foldr没有任何区别(但使用foldr'cs'将是一场灾难!),这一点并不明显。 ws'inWord'之前的爆炸对于避免空间泄漏至关重要,但{{1}}之前的爆炸不是(虽然它会略微提高性能)等等。

答案 1 :(得分:6)

  

当计算机程序使用的内存超过必要时,会发生空间泄漏。与内存泄漏相反,泄漏的内存从未被释放,空间泄漏所消耗的内存被释放,但比预期的要晚。 * Source