Haskell ByteStrings-最终将大文件加载到内存中

Haskell ByteStrings-最终将大文件加载到内存中,haskell,lazy-evaluation,bytestring,Haskell,Lazy Evaluation,Bytestring,您好 我试图理解为什么我会看到下面的程序将整个文件加载到内存中,但是如果您注释掉“(***”)下面的行,那么程序将在恒定(约1.5M)的空间中运行 编辑:文件大小约为660MB,第26列中的字段是类似“2009-10-01”的日期字符串,有一百万行。该进程在点击“getLine”时使用了大约810MB的内存 我是否正确地认为它与使用“split”拆分字符串有关,并且从文件中读取的底层ByteString无法被垃圾收集,因为它仍然被引用?但如果是这样的话,我想BS.copy可以解决这个问题。任何关

您好

我试图理解为什么我会看到下面的程序将整个文件加载到内存中,但是如果您注释掉“(***”)下面的行,那么程序将在恒定(约1.5M)的空间中运行


编辑:文件大小约为660MB,第26列中的字段是类似“2009-10-01”的日期字符串,有一百万行。该进程在点击“getLine”时使用了大约810MB的内存

我是否正确地认为它与使用“split”拆分字符串有关,并且从文件中读取的底层ByteString无法被垃圾收集,因为它仍然被引用?但如果是这样的话,我想BS.copy可以解决这个问题。任何关于如何强制计算的想法-我似乎无法将“seq”放在正确的位置来产生效果

(注意:源文件是以制表符分隔的行)

提前感谢,

凯文

modulemain其中
导入系统.IO
将限定数据.ByteString.Lazy.Char8作为BS导入
进口管制
类型记录=BS.ByteString
导入记录::字符串->IO[记录]
importRecords filename=do
liftM(map importRecord.BS.lines)(BS.readFile文件名)
导入记录::BS.ByteString->Record
导入记录txt=r
哪里
r=getField 26
getField f=BS.copy$((BS.split'\t'txt)!!f)
loopInput::[Record]->IO()
循环输入jrs=do
putStrLn$“完成”+(显示$last jrs)
hFlush stdout

x您对
last
的调用将强制列出
jrs
。为了弄清楚这一点,它必须运行整个文件,为
jrs
中的每个条目建立thunk。因为您没有计算
jrs
中的每个元素(最后一个除外),所以这些thunk与bytestring的引用挂起,因此必须保留在内存中

解决方案是强制对这些Thunk进行评估。因为我们谈论的是空间,所以我做的第一件事实际上是以较小的格式存储您的信息:

type Year   = Word16
type Month  = Word8
type Day    = Word8
data Record = Rec {-# UNPACK #-} !Year {-# UNPACK #-} !Month {-# UNPACK #-} !Day 
        deriving (Eq, Ord, Show, Read)
这将丑陋的10字节Bytestring(+约16字节的结构信息开销)减少到8字节左右

importRecord
现在必须调用
toRecord r
以获取正确的类型:

toRecord :: BS.ByteString -> Record
toRecord bs =
    case BS.splitWith (== '-') bs of
            (y:m:d:[]) -> Rec (rup y) (rup m) (rup d)
            _ -> Rec 0 0 0

rup :: (Read a) => BS.ByteString -> a
rup = read . BS.unpack
当我们从
ByteString
转换到
Record
时,我们需要计算数据,因此让我们使用包并从中定义一个NFData实例

现在我们可以开始了,我将main修改为使用
evalList
,从而强制将整个列表放在需要最后一个列表的函数之前:

main = do
    jrs <- importRecords "./tabLines"
    let jrs' = using jrs (evalList rdeepseq)
    loopInput jrs'
main=do

jrs这里似乎有两个问题:

  • 为什么内存使用取决于行(***)的存在与否
  • 为什么带(***)的内存使用量约为800MB,而不是40MB
我真的不知道该怎么说TomMD没有说的第一个;在
loopInput
循环中,
jrs
永远不能被释放,因为它需要作为
loopInput
递归调用的参数。(您知道当(***)存在时,
return()
不会做任何事情,对吗?)

至于第二个问题,我认为您是对的,InputByTestRing没有被垃圾收集。原因是,除了最后一个元素之外,您从未对列表中的元素
jrs
求值,因此它们仍然包含对原始ByteString的引用(即使它们的形式为
BS.copy…
)。我认为用
show jrs
替换
show$last jrs
会减少内存使用;是吗?或者,您可以尝试更严格的地图,如

map' f []     = []
map' f (x:xs) = ((:) $! (f $! x)) (map' f xs)

importRecords
中的
map
替换为
map'
,看看这是否会减少内存使用量。

如果有该文件或至少有一些关于该文件的统计信息,会很有帮助。我不相信您的问题不是因为将所有
[Record]
同时保存在内存中(由
putStrLn…最后一次jrs
调用强制)。整个列表必须保留在内存中,因为您在
loopInput
中传递它-如果您只传递了
上一个JR
,或者如果您在强制执行其余操作之前只消耗了列表的头,那么您可以执行常量空间增量处理。编辑:另外,做一些堆分析。文件大约有660MB,第26列中的字段是一个日期字符串,如“2009-10-01”,有一百万行。当进程点击“getLine”时,它将使用大约810MB的内存。干杯感谢您的全面回应;看到使用seq/deepSeq强制进行评估是很有见地的,但我仍然不明白为什么这个程序会在一开始就表现不好。这似乎取决于(***)下面的行是否存在;如果是,那么大的bytestring将挂在内存中。如果我将行“show(last jrs)”编辑为“show jrs”,那么这也应该强制对整个列表进行评估-正确吗?在这种情况下,程序再次在1-2M中运行,不带(***)行,但如果再次调用“loopInput jrs”,则会将文件保存在内存中。对不起,我有点胖了!使用上述
Record
定义(每个条目约24字节,4.1M个条目)和
show jrs
而不是
show$last jrs
我看到占用了300MB(记录列表约100MB,50MB?,复制GC 2倍)。我认为外部alloc只会分配64+字节的块,因此对于每个1M条目,有1个字用于LPS构造函数,1个字用于PS构造函数,1个字用于指针,2个字用于长度和偏移量,1个字用于下一个LPS字段,64+字节用于实际数据,列表包含3个字-->9*8+64=136B,136MB用于1M条目*2用于GC+潜在未知项。凯文:我之前评论中的计算结果与你800MB的使用量不符(我知道,我看到了),但它的进步。也许你可以运行一些评测,或者用一个压缩的记录(比如我的)替换你的BS.ByteString,看看情况如何?凯文:我的300M结果是f
main = do
    jrs <- importRecords "./tabLines"
    let jrs' = using jrs (evalList rdeepseq)
    loopInput jrs'
map' f []     = []
map' f (x:xs) = ((:) $! (f $! x)) (map' f xs)