3

ディープバインディングを備えた静的/字句スコープの言語があり、クロージャーを作成するとします。クロージャーは、実行したいステートメントといわゆる参照環境、またはこの投稿を引用すると、使用できる変数のコレクションで構成されます。

この参照環境は実際に実装的にどのように見えるのでしょうか? 私は最近、ObjectiveC のブロックの実装について読んでいました。著者は、バックグラウンドでスタック上のすべての変数とヒープ オブジェクトへのすべての参照のコピーを取得することを提案しています。説明では、クロージャーが作成された時点で参照環境の「スナップショット」を取得すると主張しています。

  1. それは多かれ少なかれ起こることですか、それとも私はそれを読み違えましたか?
  2. ヒープオブジェクトの別のコピーを「凍結」するために何かが行われていますか?それとも、クロージャーの作成とクロージャーの実行の間にそれらが変更された場合、クロージャーはオブジェクトの元のバージョンで動作しなくなると想定しても安全ですか?
  3. 実際にコピーが行われている場合、多くのクロージャーを作成してどこかに保存したい状況で、メモリ使用に関する考慮事項はありますか?

これらの概念の一部を誤解すると、このブログ投稿で Eric Lippert が言及しているような厄介な問題につながる可能性があると思います。クロージャーが呼び出されるまでになくなる可能性のある値型への参照を保持するのは意味がないと思うので興味深いですが、C# ではコンパイラーは変数が後で必要になり、代わりにヒープに入れます。

ほとんどのメモリ管理言語ではすべてが参照であるように思われるため、ObjectiveC は、スタック上にあるもののコピーに対処しなければならないというやや特殊な状況です。

4

2 に答える 2

3

これは、pseudo-javascriptのような構文で実行されている例です。

function f(x) {
  var y = ...;
  function g(z) {
    function h(w) {
      .... y, z, w ....
    }
    .... x, h ....
  }
  .... x, g ....
}

1つの表現は、リンクされた環境のチェーンです。つまり、クロージャは、コードポインタ、いくつかのスロット、およびそれを囲むクロージャまたはトップレベル環境への参照で構成されます。この表現では、

f = [<code>, <top-level-env>]
g = [<code>, f, x, y]
h = [<code>, g, z]

ただし、頻繁に使用されるため、すべての関数にトップレベル環境への直接参照を持たせる方がよい場合もあります。

f = [<code>, <top-level-env>]
g = [<code>, <top-level-env>, f, x, y]
h = [<code>, <top-level-env>, g, z]

(他にもバリエーションがあります。)

この表現の利点の1つは、可変変数をクロージャーに直接格納できることです。(まあ、おそらく、関数のアクティブ化をどのように表現するかによって異なります。)1つの欠点は、クロージャが深くネストされている場合、いくつかの変数が到達するまでに複数のホップを要する可能性があることです。もう1つの欠点は、クロージャがその親よりも長生きする場合(たとえば、greturns h)、この表現により、GCがほとんどまたは完全に到達できない環境フレームを収集できなくなる可能性があることです。

別の表現は「フラットクロージャ」です。各クロージャには、コードポインタとコードのすべての自由変数のスロットが含まれています。

g = [<code>, x, y]
h = [<code>, y, z]

この表現は、スペース/GCの問題を修正します。クロージャがメモリ内の別のクロージャを固定することはありません。一方、自由変数スロットは共有ではなくコピーされるため、多くの自由変数を含むネストされたクロージャーがある場合、またはネストされたクロージャーのインスタンスが多い場合は、全体的なメモリ使用量が高くなる可能性があります。また、この表現では通常、変更可能な変数をヒープに割り当てるためのストレージが必要です(ただし、実際に変更された変数の場合、および変更を自動的に書き換えることができない場合のみ)。

ハイブリッドアプローチもあります。たとえば、ほとんどフラットなクロージャを使用していても、トップレベルの環境を特別に扱う場合があります。

g = [<code>, <top-level-env>, x, y]

または、自由変数の数、ネストの深さなどに基づいて表現を選択しようとする「十分に賢い」(または少なくとも「十分に野心的な」)コンパイラを使用している場合があります。

于 2012-09-04T21:28:05.030 に答える
2

Smalltalk では、クロージャは「外部コンテキスト」への参照を保持できます。外側のコンテキストは通常​​、クロージャーを作成したメソッドのスタック フレームですが、ネストされたクロージャーの場合は、別のクロージャーである可能性があります。

外側のコンテキストへの参照を保持するクロージャーは、対応するスタックがガベージ コレクションされるのを防ぐため (私が推測するに) コストがかかります。したがって、クロージャーは本当に必要な場合にのみ外部コンテキストを参照します。

クリーン クロージャ: ローカルへの参照がないクロージャ。外部コンテキストを参照する必要はありません。

例えば[ Transcript show: 'something' ]

クロージャーのコピー: クロージャーの作成後に変更されない変数を参照するクロージャー。クロージャーが作成された時点の変数の値は、クロージャー自体にコピーされます。次に、外部コンテキストへの参照を保持する必要はありません。例えば

| | リスト |
リスト:= OrderedCollection 新しい。
1 から: 5 する: [ :i | リスト追加: i ]。

完全クロージャー: 外部コンテキストへの参照を保持するクロージャー。例えば

| | カウンター |
カウンター := 0.
1 から: 5 do: [ :i | カウンター := カウンター + 1 ]。

クロージャーが作成された後にクローズされる変数が変更された場合は、完全なクロージャーが必要ですが、ローカル以外の return の場合も同様です。非ローカル リターンについては、Neal Gafter のこのブログ投稿をお楽しみください。

また、Brian Goetz のState of the Lambdaは、今後の JDK 7 のクロージャに関するものです。とりわけ、Java の制限に固執して最終的な変数のみをキャプチャし、変更可能なローカル変数のキャプチャを禁止する理由についての議論は興味深いものでした。上記の完全閉鎖の例はサポートされません。彼らが主張する議論は、それはほとんどシリアルイディオムだということです.

于 2012-09-03T06:54:58.963 に答える