我的 Haskell 程序使用这么多内存是否有原因?

Pau*_*nes 1 memory performance haskell functional-programming haskell-stack

给定一个由空格分隔的单词输入文件,文件大小约为 64mb:

main :: IO ()
main = do
    content <- getData "data_1.txt" -- opens file, reads contents, closes handle,returns
    let tokens = words content
        mappedData = map (\token -> (token, 1)) tokens
        keySet = Set.fromList tokens
        intermediateData = map (\key -> (key, map snd (filter (\kv -> fst kv == key) mappedData))) (Set.toList keySet)
        final = map (\pair -> (fst pair, foldr (+) 0 (snd pair))) intermediateData
    print final
Run Code Online (Sandbox Code Playgroud)

content = ""
with open("data_1.txt", "r") as file:
    content = file.read()

tokens = content.split()

intermediate_data = []

for token in tokens:
    intermediate_data.append((token, 1))

keys = set()
for pair in intermediate_data:
    keys.add(pair[0])

grouped_values = []
for key in keys:
    values = [y for x, y in intermediate_data if x == key]
    grouped_values.append((key, values))

final = []
for elem in grouped_values:
    reduced = sum(elem[1])
    final.append((elem[0], reduced))

print(final)
Run Code Online (Sandbox Code Playgroud)

Haskell 程序使用 4.1 GB RAM,而 Python 程序使用 1.7 GB。它们都做几乎完全相同的事情,虽然这个例子是 100% 惰性评估,但大多数严格评估基本上根本不会提高使用率。有什么明显的事情表明我做错了吗?

我可以并行化 Haskell 程序或使用一些更高效的数据结构,但似乎存在一个潜在问题,因为 RAM 使用量比 Python 高约 2.5 倍。我想如果我使用更快的编译语言,RAM 使用量会更少。

lef*_*out 10

    \n
  1. String是一个字符链接列表。就内存占用而言,这是可怕的(每个字符 \xe2\x89\x88128 位,而 8 位对于英文文本来说就足够了)。这种类型应该只用于需要延迟处理的字符串(具体来说,逐个字符延迟处理),或者字符串太少太短以至于效率不重要(并且您确信它不会)在不寻常的用例中,在运行时不会更多)。
    \n如果不是这种情况,您应该使用ByteString(用于精确的编码控制和二进制数据),或者Text将其用作文本,这似乎是这里的情况。
  2. \n
  3. 除了单词集之外,您还以简单和带注释的形式存储多个单词列表。由于多次使用,GHC 可能无法将它们融合在一起,并且至少其中一个列表将完全在内存中表示,而不是被增量垃圾收集。这是否真的重要取决于单个单词的长度:如果这些是足够长的字符串,那么实际的字符存储无论如何都会主导内存使用。
  4. \n
  5. 不要用于foldr (+)计算总和。使用 simple sum,它内置了更好的严格性,可以提高效率。(这里实际上并不重要。)
  6. \n
  7. 您的算法效率非常低,按顺序循环所有单词和单词集。这是完全没有必要的,因为 a 的创建Set已经涉及将相同的单词合并在一起。如果您切换到相似的Map,您可以利用该合并来累积计数器,因此永远不需要存储所有这些冗余的1- 。
  8. \n
\n

尝试这个:

\n
import qualified Data.Text as T\nimport qualified Data.Map as M\n\nmain :: IO ()\nmain = do\n    content <- T.readFile "data_1.txt"\n    let tokens = T.words content\n        mappedData = Map.fromListWith (+)\n            $ map (\\token -> (token, 1)) tokens\n        final = Map.toList mappedData\n    print final\n
Run Code Online (Sandbox Code Playgroud)\n

...或更短

\n
{-# LANGUAGE TupleSections #-}\n\nmain = do\n    content <- T.readFile "data_1.txt"\n    print . Map.toList . Map.fromListWith (+)\n        $ (,1) <$> T.words content\n
Run Code Online (Sandbox Code Playgroud)\n

实际上使用readFile仍然使这远非理想,因为它Char在引擎盖下使用了旧的 s。在这种情况下,字符列表应该通过/增量垃圾收集来融合,这样它们就不会同时驻留在内存中,只有结果Text会同时驻留在内存中。

\n

您甚至可以更进一步,甚至不阅读全文,而只是懒惰地阅读。这应该会使其内存更加精简。

\n
import qualified Data.ByteString.Lazy as BL\nimport qualified Data.Text.Lazy as TL\nimport qualified Data.Text.Lazy.Encoding as TL\n\nmain = do\n    content <- TL.decodeUtf8 <$> BL.readFile "data_1.txt"\n    print . Map.toList . Map.fromListWith (+)\n        $ (,1) . TL.toStrict <$> TL.words content\n
Run Code Online (Sandbox Code Playgroud)\n

在此版本中,运行时将能够在大多数其他单词读入内存之前对第一个单词进行垃圾收集,并且只有重复减少的形式需要永久存储。

\n

但请注意,惰性 IO 很繁琐,很容易意外地例如在真正完成文件之前关闭文件。如果在实际应用程序中使用它,您应该确保在移交控制权之前对结果列表进行完全评估/在内存中。

\n

  • 使用:`foldl (\acc word -&gt; HM.insertWith (+) word 1 acc) HM.empty $ T.words fileText` 使用 HashMap 而不是 Map 并在foldr上使用foldl,内存使用量约为350 MB,没有明显的峰值打印时! (2认同)