为什么Seq在迭代大型csv文件时会给堆栈溢出

时间:2012-03-09 15:16:30

标签: f# sequence stack-overflow

我有一个csv文件,其结构如下:

  1. 第一行是标题行
  2. 其余行是数据行, 每个都有相同数量的逗号,所以我们可以想到数据 列条款
  3. 我编写了一个小脚本来遍历文件的每一行,并返回一系列元组,其中包含列标题和该列中最大字符串数据的长度:

    let getColumnInfo (fileName:string) =
        let delimiter = ','
    
        let readLinesIntoColumns (sr:StreamReader) = seq { 
            while not sr.EndOfStream do     
                yield sr.ReadLine().Split(delimiter) |> Seq.map (fun c -> c.Length )
        }
    
        use sr = new StreamReader(fileName)     
        let headers = sr.ReadLine().Split(delimiter) 
        let columnSizes =
            let initial = Seq.map ( fun h -> 0 ) headers
            let toMaxColLengths (accumulator:seq<int>) (line:seq<int>)  = 
                 let chooseBigger a b = if a > b then a else b
                 Seq.map2 chooseBigger accumulator line
            readLinesIntoColumns sr |> Seq.fold toMaxColLengths initial
        Seq.zip headers columnSizes;
    

    这适用于小文件。然而,当它试图处理一个大文件(> 75 Mb)时,它会使用StackOverflow异常来填充fsi。如果我删除该行

    Seq.map2 chooseBigger accumulator line
    

    程序完成。

    现在,我的问题是:为什么F#用尽了堆栈?我对F#中的序列的理解是整个序列不保存在内存中,只保存在正在处理的元素中。因此,我预计已处理的行不会保留在堆栈中。我的误解在哪里?

3 个答案:

答案 0 :(得分:6)

我认为这是一个很好的问题。这是一个更简单的复制品:

let test n =
    [for i in 1 .. n -> Seq.empty]
    |> List.fold (Seq.map2 max) Seq.empty
    |> Seq.iter ignore

test创建一系列空序列,按行计算max,然后迭代生成的(空)序列。你会发现n的值很高,这会导致堆栈溢出,即使根本没有任何值可以迭代!

解释原因有点棘手,但这里有点刺痛。问题在于,当您对序列进行折叠时,Seq.map2将返回一个新序列,该序列将其工作推迟到枚举之前。因此,当您尝试迭代生成的序列时,最终会回调到深层计算n层的链。

正如丹尼尔解释的那样,你可以通过急切地评估结果序列(例如将其转换为列表)来逃避这种情况。

修改

这是尝试进一步解释出现了什么问题。当您致电Seq.map2 max s1 s2时,s1s2实际上都未被列举;你得到一个新的序列,当枚举时,它将枚举它们并比较产生的值。因此,如果我们执行以下操作:

let s0 = Seq.empty
let s1 = Seq.map2 max Seq.emtpy s0
let s2 = Seq.map2 max Seq.emtpy s1
let s3 = Seq.map2 max Seq.emtpy s2
let s4 = Seq.map2 max Seq.emtpy s3
let s5 = Seq.map2 max Seq.emtpy s4
...

然后对Seq.map2的调用总是立即返回并使用常量堆栈空间。 然而,枚举s5需要枚举s4,这需要枚举s3等。这意味着枚举s99999将构建一个巨大的调用堆栈,看起来有点像:

...
(s99996's enumerator).MoveNext()
(s99997's enumerator).MoveNext()
(s99998's enumerator).MoveNext()
(s99999's enumerator).MoveNext()

我们会得到堆栈溢出。

答案 1 :(得分:2)

您的代码包含很多序列,很难推理。我猜这就是绊倒你的原因。你可以使这更加简单和有效(渴望并非全是坏事):

let getColumnInfo (fileName:string) =
  let delimiter = ','
  use sr = new StreamReader(fileName)
  match sr.ReadLine() with
  | null | "" -> Array.empty
  | hdr ->
    let cols = hdr.Split(delimiter)
    let counts = Array.zeroCreate cols.Length
    while not sr.EndOfStream do
      sr.ReadLine().Split(delimiter)
      |> Array.iteri (fun i fld ->
        counts.[i] <- max counts.[i] fld.Length)
    Array.zip cols counts

这假设所有行都是非空的并且具有相同的列数。

您可以通过将此行更改为:

来修复您的功能
Seq.map2 chooseBigger accumulator line |> Seq.toList |> seq

答案 2 :(得分:1)

  

为什么F#用尽了堆栈?我对F#中的序列的理解是整个序列不保存在内存中,只保存在正在处理的元素中。因此,我预计已处理的行不会保留在堆栈中。我的误解在哪里?

线条本身并没有占用你的筹码空间。问题是你不小心编写了一个函数,它建立了一个巨大的未评估计算(thunk的树),当它被评估时堆栈溢出,因为它使非尾部调用O(n)深。每当您从其他序列构建序列并且不强制评估任何序列时,这往往会发生。