2013-04-14 54 views
27

我正在讀O'Reilly的Clojure編程書。Clojure頭保留

我來過一個頭保留的例子。 第一個例子保留參照d(我相信),所以它不被垃圾收集:

(let [[t d] (split-with #(< % 12) (range 1e8))] 
    [(count d) (count t)]) 
;= #<OutOfMemoryError java.lang.OutOfMemoryError: Java heap space> 

在第二個例子中犯規留住它,所以它去沒有問題:

(let [[t d] (split-with #(< % 12) (range 1e8))] 
    [(count t) (count d)]) 
;= [12 99999988] 

什麼我不在這種情況下,爲什麼不能到達這裏。 如果我嘗試只返回[(count d)],像這樣:

(let [[t d] (split-with #(< % 12) (range 1e8))] 
    [(count d)]) 

似乎創造同樣的內存問題。

此外,我記得在每種情況下讀取count實現/評估一個序列。 所以,我需要澄清。

如果我先嚐試返回(count t),那麼如果我完全不返回它,那麼如何更快/更高的內存效率呢? 什麼&爲什麼在這種情況下被保留?

回答

25

在第一個和最後一個例子中,傳遞給split-with的原始序列在內存中全部實現時被保留;因此OOME。這種情況發生的方式是間接的;直接保留的是t,而原始序列被t,一個懶惰seq,保持在其未實現狀態

方法t導致原始序列被保持如下。在實現之前,t是一個LazySeq對象,存儲一個thunk,可以在某個時候調用它來實現t;這個thunk需要將一個指向原始序列參數的指針存儲到split-with,然後才能將它傳遞給take-while - 請參閱split-with的實現。一旦實現t,thunk變爲符合GC的條件(保存在LazySeq對象中的字段設置爲nullt不再擁有巨大輸入序列的頭部。

輸入序列本身完全由(count d)實現,需要實現d,因此需要原始輸入序列。

移動到爲什麼t被保留:

在第一種情況下,這是因爲(count d)(count t)之前評估。由於Clojure從左到右評估這些表達式,本地t需要在第二次調用時進行計數,並且由於它恰好保持在一個巨大的seq上(如上所述),因此導致了OOME。

只返回(count d)的最後一個例子理想情況下不應該等於t;不是這種情況的原因有些微妙,並且通過參考第二個例子得到了最好的解釋。

第二個例子發生在做工精細,(count t)進行評估,因爲後,不再需要t。 Clojure的編譯器注意到這一點,並使用一個聰明的技巧與所取得的count呼叫同時具有本地重置爲nil。 Java代碼的關鍵部分的功能類似f(t, t=null),因此當前值t被傳遞給相應的函數,但是在控制交給f之前清除了本地,因爲這是作爲表達式t=null的副作用發生的是f的一個參數;顯然這裏Java的從左到右的語義是這個工作的關鍵。

回到最後一個例子,這是行不通的,因爲t實際上沒有任何地方使用和未使用的當地人不被當地人清理過程處理。 (清除發生在最後一次使用時;如果程序中沒有這樣的一個點,則沒有清除。)

至於count實現惰性序列:它必須這樣做,因爲沒有一般的方法預測懶惰seq的長度而不意識到它。

+0

你說「本地T需要流連第二個電話來算,並且由於它發生在一個巨大的序列,這導致OOME點。」但't'只是12項順序。 如何處置't'扮演這樣一個巨大的作用,如果它是12個項目序列。而拿着'D'內存心不是一個問題,但它是'(範圍1E8)' –

+1

好了,'噸其餘全部'在以某種方式使用之前是一個未實現的懶惰seq,它在內部持有一些代碼,在某些時候可能需要實現't'。這段代碼需要在傳遞給'split-with'的原始序列中保存一個指針,所以它可以將它傳遞給'take-while'(參見'split-with'的實現)。感謝您的評論 - 這應該是答案,我會在編輯的一部分 –

+0

所以你說的話是,'t'舉行,它擁有實際內存中的整個原始序列? 因此,雖然'(count d)'正在計算,與我們clojure並行,保持整個原始序列在內存中實現? –

22

回答@MichałMarczyk,雖然正確,但有點難以理解。我發現this post on Google Groups更容易掌握。

我是這樣理解的:

步驟1創建懶序列:(range 1e8)。值尚無法實現,我將其標示爲asterixes(*):

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * ... * * * 

步驟2創建兩個懶seqences這是「窗口」,通過它,你看原來,巨大的懶序列。第一個窗口只含有12個元素(t),元素的其他休息(d):

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * ... * * * 
t t t t t t t t t t t t t d d d d d d d d d d d d d d d d d ... d d d 

第3步 - 內存不足的情況下的 - 你評估[(count d) (count t)]。所以,首先你計算d中的元素,然後在t。會發生什麼事是,你會經過的所有值起始於d的第一要素,實現他們(標記爲!):

* * * * * * * * * * * * * ! * * * * * * * * * * * * * * * * ... * * * 
t t t t t t t t t t t t t d d d d d d d d d d d d d d d d d ... d d d 
         ^
         start here and move right -> 

* * * * * * * * * * * * * ! ! * * * * * * * * * * * * * * * ... * * * 
t t t t t t t t t t t t t d d d d d d d d d d d d d d d d d ... d d d 
          ^

* * * * * * * * * * * * * ! ! ! * * * * * * * * * * * * * * ... * * * 
t t t t t t t t t t t t t d d d d d d d d d d d d d d d d d ... d d d 
          ^

        ... 

; this is theoretical end of counting process which will never happen 
; because of OutOfMemoryError 
* * * * * * * * * * * * * ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ... ! ! ! 
t t t t t t t t t t t t t d d d d d d d d d d d d d d d d d ... d d d 
                    ^

問題是,所有的實現值(!)被保留,因爲收集頭(前12個元素)仍然需要 - 我們仍然需要評估(count t)。這會消耗大量內存,導致JVM崩潰。

第3步 - 有效場景 - 這次您評估[(count t) (count d)]。因此,我們首先要在更小的,序列數元素:

! * * * * * * * * * * * * * * * * * * * * * * * * * * * * * ... * * * 
t t t t t t t t t t t t t d d d d d d d d d d d d d d d d d ... d d d 
^ 
start here and move right -> 

         ! * * * * * * * * * * * * * * * * * ... * * * 
t t t t t t t t t t t t t d d d d d d d d d d d d d d d d d ... d d d 
         ^

然後,我們算上d序列元素。編譯器知道從t元素沒有必要了,所以它可以收集的垃圾他們騰出內存:

      ! * * * * * * * * * * * * * * * * ... * * * 
t t t t t t t t t t t t t d d d d d d d d d d d d d d d d d ... d d d 
         ^

          ! * * * * * * * * * * * * * * * ... * * * 
t t t t t t t t t t t t t d d d d d d d d d d d d d d d d d ... d d d 
          ^

        ... 

                  ...  ! 
t t t t t t t t t t t t t d d d d d d d d d d d d d d d d d ... d d d 
                    ^

現在我們可以看到,因爲是不再需要從t元素,編譯器能夠清晰的記憶,因爲它經歷了大的序列。

+2

非常感謝kamituel。我明白MichałMarczyk的出色答案,但這更清楚。 – Mars