当我从文件中读取地图时,Haskell需要比Python更多的内存.为什么?

Ale*_*nov 7 haskell

我在Python中有这个简单的代码:

input = open("baseforms.txt","r",encoding='utf8')
S = {}
for i in input:
    words = i.split()
    S.update( {j:words[0] for j in words} )
print(S.get("sometext","not found"))
print(len(S))
Run Code Online (Sandbox Code Playgroud)

它需要300MB的工作量."baseforms.txt"大小为123M.我在Haskell中编写了相同的代码:

{-# LANGUAGE OverloadedStrings #-}
import qualified Data.Map as M
import qualified Data.ByteString.Lazy.Char8 as B
import Data.Text.Lazy.Encoding(decodeUtf8)
import qualified Data.Text.Lazy as T
import qualified Data.Text.Lazy.IO as I
import Control.Monad(liftM)

main = do
    text <- B.readFile "baseforms.txt"
    let m = (M.fromList . (concatMap (parseLine.decodeUtf8))) (B.lines text)
    print (M.lookup "sometext" m)
    print (M.size m)
    where
        parseLine line = let base:forms = T.words line in [(f,base)| f<-forms]
Run Code Online (Sandbox Code Playgroud)

它需要544 MB,并且比Python版本慢.为什么?是否可以优化Haskell版本?

Die*_*Epp 20

Haskell版本中发生了很多事情,这在Python版本中没有发生.

  • readFile使用懒惰的IO,一般来说有点奇怪.我一般会避免懒惰的IO.

  • 该文件作为字节串被分成多行,然后解码为UTF-8.考虑到TextIO功能的存在,这似乎有点不必要.

  • Haskell版本使用树(Data.Map),而Python版本使用哈希表.

  • 字符串都是懒惰的,如果它们相对较短则可能没有必要.懒字符串每个字符串有几个单词的开销,这可以加起来.你可以融合懒惰的字符串,或者你可以一次读取文件,或者你可以使用像管道这样的东西.

  • GHC使用复制收集器,而默认的Python实现使用malloc()引用计数和偶尔的GC.仅此事实就可以解释内存使用量的巨大差异,具体取决于您的程序.

  • 谁知道在Haskell版本中创建了多少个thunk.

  • 您是否已启用优化尚不得而知.

  • 知道 Haskell版本的速度有多慢.

  • 我们没有您的数据文件,因此我们无法自行测试.


Mic*_*ael 3

虽然有点晚了,但我研究了一下,认为 Dietrich Epp 的说法是正确的,但可以稍微简化一下。请注意, Python 文件中似乎没有进行任何真正的 Python编程:它正在编排一个非常简单的 C 字符串操作调用序列,然后调用 C 哈希表实现。(这通常是非常简单的 python 与 Haskell 基准测试的问题。)相比之下,Haskell 正在构建一个巨大的持久性Map,这是一棵奇特的树。所以这里的主要反对点是 C 与 Haskell,以及具有破坏性更新的哈希表与持久映射。由于输入文件中几乎没有重叠,因此您正在构建的树包含输入字符串中的所有信息,其中一些信息是重复的,然后用一堆 Haskell 构造函数重新排列。我认为这是您遇到的警报的来源,但可以解释。

比较这两个文件,其中一个使用 ByteString:

import qualified Data.Map as M
import qualified Data.ByteString.Char8 as B
main = do m <- fmap proc (B.readFile "baseforms.txt")
          print (M.lookup (B.pack "sometext") m)
          print (M.size m)
proc = M.fromList  . concatMap (\(a:bs) -> map (flip (,) a) bs) 
       . map B.words . B.lines
Run Code Online (Sandbox Code Playgroud)

另一个是文本化的等价物:

import qualified Data.Map as M
import qualified Data.ByteString.Char8 as B
import Data.Text.Encoding(decodeUtf8)
import qualified Data.Text as T

main = do
    m <- fmap proc (B.readFile "baseforms.txt")
    print (M.lookup (T.pack "sometext") m)
    print (M.size m)

proc = M.fromList  . concatMap (\(a:bs) ->  map (flip (,) a) bs)
       . map T.words . T.lines  . decodeUtf8
Run Code Online (Sandbox Code Playgroud)

在我的机器上,python/C 需要不到 6 秒,字节串文件需要 8 秒,文本文件需要 10 多秒。

字节串实现似乎比 python 使用更多的内存,文本实现显然更多。文本实现需要更多时间,因为它当然添加了对文本的转换,然后使用文本操作来打破字符串和文本比较来构建地图。

下面尝试分析一下文本案例中的记忆现象。首先我们有内存中的字节串(130m)。一旦文本被构造出来(~250m,从 中发生的事情来判断是不科学的top),在我们构造树的时候,字节串就会被垃圾收集。最后,文本树(看起来大约 380m)比字节串树(大约 260m)使用更多的内存,因为树中的文本片段更大。整个程序使用更多,因为在树构建期间内存中保存的文本本身更大。粗略地说:每一位空白都被转换为一个树构造函数和两个文本构造函数,以及该行的第一个“单词”的文本版本以及下一个单词的文本表示形式。在这两种情况下,构造函数的权重似乎约为 130m,因此在构建树的最后一刻,我们在字节串情况下使用 130m + 130m + 130m = 390m,以及 250m + 130m + 250m = 630m在文本情况下。