2012-03-09 70 views
4

迭代時給堆棧溢出我有以下結構的CSV文件:爲什麼SEQ通過大csv文件

  1. 第一行是頭行
  2. 剩餘線是數據線,每一 與相同數量的逗號,所以我們可以在列 方面認爲數據的

我已經寫了一個小腳本去通過文件的每一行,並返回一個包含列標題元組序列和日數據該列中的最大的字符串電子長度:

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異常。如果我刪除線

Seq.map2 chooseBigger accumulator line 

該程序完成。

現在,我的問題是這樣的:爲什麼F#使用堆棧?我對F#中序列的理解是,整個序列不保存在內存中,只保留正在處理的元素。因此,我預計已經處理過的行將不會保留在堆棧上。我的誤解在哪裏?

+0

75Mb文件中有多少行和列? – pad 2012-03-09 15:30:35

+0

我不知道。至少50,000。這並不是我想讓它發揮作用,我更加好奇爲什麼我對F#的理解是有缺陷的。 (儘管它也能很好地工作) – Aidan 2012-03-09 15:32:57

+0

如果你這樣做會發生什麼:'Seq.map2 chooseBigger accumulator line |> Seq.toList |> seq'? – Daniel 2012-03-09 16:47:03

回答

6

我覺得這是一個很好的問題。下面是一個更簡單的REPRO:

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

test。外幣空序列的序列,通過行計算最大值,然後用所得到的(空)序列迭代。你會發現它的值高達n這會導致堆棧溢出,即使沒有任何值可以重複執行!

解釋原因有點棘手,但是這裏有一個刺點。問題在於,當你迭代序列時,Seq.map2正在返回一個新的序列,將其工作推遲到枚舉。因此,當您嘗試遍歷結果序列時,最終會調用深層計算鏈。如Daniel解釋,你可以通過熱切地評估結果序列(例如將它轉換爲列表)來避免這種情況。

編輯

下面是一個試圖進一步解釋什麼錯。當您撥打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() 

,我們會得到一個堆棧溢出。

+0

顯然,我的邏輯是錯誤的,因爲這適用於較小的文件。我開始沿着和你一樣的路線走,但是我不明白它是如何導致堆棧溢出(可能是內存不足)。 – Daniel 2012-03-09 18:08:06

+0

@丹尼爾 - 我試圖擴展我的解釋。讓我知道如果它清除它。 – kvb 2012-03-09 18:23:47

+0

有道理。感謝您的解釋。 – Daniel 2012-03-09 18:34:54

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 
+0

它不直接回答問題,是嗎? – pad 2012-03-09 15:48:40

+0

不,它沒有,但它指出了問題的可能來源(太多懶惰)。採取更簡單的方法可能會避免難以發現的錯誤。 – Daniel 2012-03-09 15:52:28

+1

如果我想要有人指出懶惰是我的問題的根源,我會問我的妻子:-) – Aidan 2012-03-09 15:57:35

1

爲什麼F#使用堆棧?我對F#中序列的理解是,整個序列不保存在內存中,只保留正在處理的元素。因此,我預計已經處理過的行將不會保留在堆棧上。我的誤解在哪裏?

線條本身不會佔用您的堆棧空間。問題在於你意外地寫了一個函數,它建立了一個巨大的未評估計算(thunks樹),當它被評估時會溢出,因爲它使得非尾部調用O(n)更深。當你從其他序列構建序列並且不強制評估任何東西時,這往往會發生。

相關問題