29

私はClojureが好きです。言語について私を悩ませていることの1つは、レイジーシーケンスがどのように実装されているか、またはそれらがどのように機能するかがわからないことです。

怠惰なシーケンスは、要求されたシーケンス内のアイテムのみを評価することを知っています。これはどのように行いますか?

  • 怠惰なシーケンスが非常に効率的で、スタックをあまり消費しないのはなぜですか?
  • 再帰呼び出しを遅延シーケンスでラップし、大規模な計算でスタックオーバーフローを発生させないようにするにはどうすればよいですか?
  • レイジーシーケンスは、それが行うことを実行するためにどのようなリソースを消費しますか?
  • 怠惰なシーケンスはどのシナリオで非効率的ですか?
  • 怠惰なシーケンスが最も効率的なシナリオはどれですか?
4

3 に答える 3

32

これをやろう。

レイジーシーケンスは、要求されたシーケンス内のアイテムのみを評価することを知っていますが、これはどのように行われますか?

レイジーシーケンス(以降、私はLPであるため、LS、またはレイジーパーソン)はパーツで構成されます。評価されたシーケンスのヘッド、または部分(実際には32個の要素が一度に評価されます。Clojure1.1の時点で、1.2と思います)の後には、基本的にチャンクであるサンクと呼ばれるものが続きます。呼び出されるのを待っている情報(シーケンスを作成する関数の残りの部分、未評価と考えてください)。呼び出されると、サンクは要求された量を評価し、必要に応じてコンテキストを使用して新しいサンクが作成されます(すでに呼び出されている量なので、以前の場所から再開できます)。

つまり、あなたは–整数の怠惰なシーケンスである(take 10 (whole-numbers))と仮定します。whole-numbersつまり、サンクの評価を10回強制することになります(ただし、内部的には、最適化によっては少し異なる場合があります。

レイジーシーケンスが非常に効率的で、スタックをあまり消費しない理由は何ですか。

前の答えを読むと、これはより明確になります(私は願っています):特に何かを要求しない限り、何も評価されません。何かを呼び出すと、シーケンスの各要素を個別に評価してから破棄できます。

シーケンスが怠惰でない場合、多くの場合、それはその頭を保持しており、ヒープスペースを消費します。怠惰な場合は、後続の計算には必要ないため、計算されてから破棄されます。

再帰呼び出しを遅延シーケンスでラップし、大規模な計算でスタックオーバーフローを発生させないようにするにはどうすればよいですか。

前の回答を参照して検討してください:lazy-seqマクロ(ドキュメントから)は

will invoke the body only the first time seq
is called, and will cache the result and return it on all subsequent
seq calls.

filter再帰を使用するクールなLSの関数を確認してください。

(defn filter
  "Returns a lazy sequence of the items in coll for which
  (pred item) returns true. pred must be free of side-effects."
  [pred coll]
  (let [step (fn [p c]
                 (when-let [s (seq c)]
                   (if (p (first s))
                     (cons (first s) (filter p (rest s)))
                     (recur p (rest s)))))]
    (lazy-seq (step pred coll))))

レイジーシーケンスは、それが行うことを実行するためにどのようなリソースを消費しますか?

ここで何を求めているのかよくわかりません。LSにはメモリとCPUサイクルが必要です。彼らはスタックを強打し続けず、シーケンス要素を取得するために必要な計算の結果でスタックを埋めます。

どのシナリオで怠惰なシーケンスが非効率的ですか?

計算が速く、あまり使用されない小さなシーケンスを使用している場合、作成するのに別の2文字が必要になるため、LSにするのは非効率的です。

真面目な話ですが、非常にパフォーマンスの高いものを作ろうとしているのでない限り、LSが最適です。

どのシナリオでレイジーシーケンスが最も効率的ですか?

巨大なseqを扱っていて、それらの断片だけを使用している場合、それはそれらを使用することから最大の利益を得るときです。

実際、利便性、理解のしやすさ(一度コツをつかんだら)、コードについての推論、および速度の観点から、非LSよりもLSを使用する方が常に優れています。

于 2010-07-14T14:55:34.663 に答える
16

怠惰なシーケンスは、要求されたシーケンス内のアイテムのみを評価することを知っていますが、これはどのように行われますか?

以前に投稿された回答は、この部分を説明するのにすでに良い仕事をしていると思います。怠惰なシーケンスの「強制」は暗黙的であり、パレンフリーであることを追加するだけです。:-)-関数呼び出し; おそらく、それについてのこの考え方は、いくつかのことをより明確にするでしょう。また、レイジーシーケンスの強制には、隠されたミューテーションが含まれることに注意してください。強制されるサンクは、値を生成し、それをキャッシュに格納し(ミューテーション!)、実行可能コードを破棄する必要があります。 。

怠惰なシーケンスは、要求されたシーケンス内のアイテムのみを評価することを知っていますが、これはどのように行われますか?

怠惰なシーケンスが非常に効率的で、スタックをあまり消費しないのはなぜですか?

レイジーシーケンスは、それが行うことを実行するためにどのようなリソースを消費しますか?

代わりにヒープを消費するため、スタックを消費しません。レイジーシーケンスは、ヒープ上に存在するデータ構造であり、必要に応じてより多くのデータ構造を生成するために呼び出すことができる実行可能コードの小さなビットが含まれています。

再帰呼び出しを遅延シーケンスでラップし、大規模な計算でスタックオーバーフローを発生させないようにするにはどうすればよいですか?

まず、dbyrneが述べたように、サンク自体が非常に深くネストされた呼び出し構造でコードを実行する必要がある場合、レイジーシーケンスを操作するときにSOを取得できます。

ただし、ある意味では、末尾再帰の代わりに遅延シーケンスを使用できます。これが機能する程度まで、SOの回避に役立つと言えます。実際、かなり重要なことですが、レイジーシーケンスを生成する関数は末尾再帰であってはなりません。怠惰なseqプロデューサーによるスタックスペースの節約は、前述のスタック->ヒープ転送から生じ、末尾再帰的にそれらを書き込もうとすると、問題が発生するだけです。

重要な洞察は、レイジーシーケンスは、最初に作成されたときにアイテムを保持しないオブジェクトであるということです(厳密なシーケンスは常に保持します)。関数がレイジーシーケンスを返す場合、強制が行われる前に、この「レイジーシーケンスオブジェクト」のみが呼び出し元に返されます。したがって、レイジーシーケンスを返した呼び出しで使用されたスタックフレームは、強制が行われる前にポップされます。プロデューサー関数の例を見てみましょう。

(defn foo-producer [] ; not tail recursive...
  (lazy-seq
    (cons :foo        ; because it returns the value of the cons call...
           (foo-producer)))) ; which wraps a non-tail self-call

これが機能するのは、がすぐに戻るためです。したがって、すぐlazy-seqに戻り、外部呼び出しで使用されたスタックフレームがすぐにポップされます。の内側の呼び出しは、サンクであるシーケンスの一部に隠されています。そのサンクが強制された場合、スタック上の独自のフレームを一時的に使い果たしますが、上記のようにすぐに戻ります。(cons :foo (foo-producer))foo-producerfoo-producerrest

チャンキング(dbyrneで言及)は、各ステップでより多くの要素が生成されるため、この図をわずかに変更しますが、原則は同じです。各ステップは、レイジーシーケンスの対​​応する要素が生成されるときにスタックを使い果たします。そのスタックは、さらに強制が行われる前に再利用されます。

怠惰なシーケンスはどのシナリオで非効率的ですか?

怠惰なシーケンスが最も効率的なシナリオはどれですか?

とにかくすべてを一度に保持する必要がある場合は、怠惰になる意味はありません。レイジーシーケンスは、チャンクされていない場合はすべてのステップで、チャンクされている場合はすべてのチャンク(32ステップごとに1回)でヒープ割り当てを行います。これを回避すると、状況によってはパフォーマンスが向上する可能性があります。

ただし、レイジーシーケンスにより、パイプラインモードのデータ処理が可能になります。

(->> (lazy-seq-producer)               ; possibly (->> (range)
     (a-lazy-seq-transformer-function) ;               (filter even?)
     (another-transformer-function))   ;               (map inc))

これを厳密に行うと、とにかく大量のヒープが割り当てられます。これは、中間結果を次の処理ステージに渡すために保持する必要があるためです。さらに、すべてを維持する必要がありますが、これは(range)、無限のシーケンスの場合には実際には不可能です。-そしてそれが可能な場合、それは通常非効率的です。

于 2010-07-14T17:13:29.980 に答える
8

もともと、Clojureのレイジーシーケンスは、必要に応じてアイテムごとに評価されていました。Clojure 1.1では、パフォーマンスを向上させるためにチャンクシーケンスが追加されました。アイテムごとの評価の代わりに、32要素の「チャンク」が一度に評価されます。これにより、遅延評価で発生するオーバーヘッドが削減されます。また、clojureが基盤となるデータ構造を利用できるようにします。たとえば、PersistentVector32個の要素配列のツリーとして実装されます。つまり、要素にアクセスするには、適切な配列が見つかるまでツリーをトラバースする必要があります。チャンク化されたシーケンスでは、配列全体が一度に取得されます。これは、ツリーを再トラバースする必要がある前に、32個の要素のそれぞれを取得できることを意味します。

完全な怠惰が必要な状況でアイテムごとの評価を強制する方法を提供することについての議論がありました。ただし、まだ言語に追加されていないと思います。

再帰呼び出しを遅延シーケンスでラップし、大規模な計算でスタックオーバーフローを発生させないようにするにはどうすればよいですか?

意味の例はありますか?lazy-seqに再帰的にバインドしている場合は、間違いなくスタックオーバーフローが発生する可能性があります

于 2010-07-14T14:37:33.980 に答える