29

O'Reilly の Clojure プログラミングの本を読んでいます。

頭の保持の例に出くわしました。最初の例はd(私が推測する) への参照を保持しているため、ガベージ コレクションは行われません。

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

2 番目の例では保持されないため、問題ありません。

(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)、まったく返さない場合よりも高速/メモリ効率が高くなりますか? そして、どのような場合に何となぜ保持されるのでしょうか?

4

3 に答える 3

26

最初の例と最後の例の両方で、渡された元のシーケンスsplit-withは保持されますが、メモリ内で完全に実現されます。したがって、OOME。これが起こる方法は間接的です。直接保持されるのは ですがt、元のシーケンスは、未実現状態tの遅延シーケンスによって保持されています。

t元のシーケンスを保持する方法は次のとおりです。実現される前に、ある時点で実現するために呼び出される可能性のあるサンクを格納するオブジェクトtです。このサンクは、元のシーケンス引数へのポインタをに渡す前に、そのポインタを格納する必要があります。の実装を参照してください。が実現されると、サンクは GC の対象になり (オブジェクト内でサンクを保持するフィールドが に設定されます) 、huge 入力 seq の先頭を保持しなくなります。LazySeqtsplit-withtake-whilesplit-withtLazySeqnullt

入力 seq 自体は によって完全に実現されており(count d)、これは を実現する必要があるためd、元の入力 seq は

tが保持されている理由に移ります。

最初のケースでは、これは(count d)gets が の前に評価されるため(count t)です。Clojure はこれらの式を左から右に評価するため、ローカルtは 2 回目の呼び出しをカウントするためにぶらつく必要があり、(上記で説明したように) たまたま巨大な seq を保持しているため、OOME につながります。

のみが返される最後の例は、(count d)理想的には保持しないでくださいt。そうでない理由はやや微妙であり、2 番目の例を参照することで最もよく説明されます。

2 番目の例はたまたま問題なく動作します。なぜなら、 after(count t)が評価され、t不要になったからです。Clojure コンパイラはこれに気付き、巧妙なトリックを使用して、呼び出しとnil同時にローカルをリセットします。countJava コードの重要な部分は のようなf(t, t=null)ことを行い、 の現在の値がt適切な関数に渡されますが、制御が に渡される前にローカルがクリアされます。これは、 への引数でfある式の副作用として発生するためです。 ; ここで明らかに、Java の左から右へのセマンティクスがこの作業の鍵となります。t=nullf

最後の例に戻ると、これは機能しませtん。実際にはどこでも使用されておらず、未使用のローカルはローカルのクリア プロセスによって処理されないためです。(クリアは最後に使用した時点で発生します。プログラムにそのようなポイントがない場合、クリアはありません。)

count遅延シーケンスの実現に関しては、遅延シーケンスの長さを認識せずに予測する一般的な方法がないため、それを行う必要があります。

于 2013-04-14T02:47:48.870 に答える