Haskellの実装がなぜGCを使用するのか、私は興味があります。
純粋な言語でGCが必要になるケースは考えられません。コピーを減らすための最適化だけですか、それとも実際に必要ですか?
GCが存在しない場合にリークするコード例を探しています。
Haskellの実装がなぜGCを使用するのか、私は興味があります。
純粋な言語でGCが必要になるケースは考えられません。コピーを減らすための最適化だけですか、それとも実際に必要ですか?
GCが存在しない場合にリークするコード例を探しています。
回答:
他の人がすでに指摘したように、Haskellは自動で動的なメモリ管理を必要とします。手動のメモリ管理は安全でないため、自動メモリ管理が必要です。一部のプログラムでは、オブジェクトの存続期間は実行時にのみ決定できるため、動的メモリ管理が必要です。
たとえば、次のプログラムについて考えてみます。
main = loop (Just [1..1000]) where
loop :: Maybe [Int] -> IO ()
loop obj = do
print obj
resp <- getLine
if resp == "clear"
then loop Nothing
else loop obj
このプログラムでは[1..1000]
、ユーザーが「クリア」と入力するまで、リストをメモリに保持する必要があります。したがって、これの寿命は動的に決定される必要があり、これが動的メモリ管理が必要な理由です。
つまり、この意味では、自動動的メモリ割り当てが必要であり、実際にはこれが意味します。はい、Haskellにはガベージコレクタが必要です。
ガベージコレクターは必要ですが、コンパイラーがガベージコレクションよりも安価なメモリ管理スキームを使用できるいくつかの特別なケースを見つけようとする場合があります。たとえば、
f :: Integer -> Integer
f x = let x2 = x*x in x2*x2
(ガベージコレクターが割り当てを解除するのを待つのではなく)コンパイラが戻りx2
時に安全に割り当てを解除できることを検出することを期待するかもしれません。基本的に、コンパイラーがエスケープ分析を実行して、割り当てをガベージコレクションされたヒープに変換し、可能な限りスタック上の割り当てに変換するように求めています。f
x2
これは無理ではありません。jhchaskellコンパイラがこれを行いますが、GHCは行いません。サイモンマーロー氏によると、GHCの世代別ガベージコレクターでは、エスケープ分析はほとんど不要です。
jhcは実際には、領域推論と呼ばれる高度な形式のエスケープ分析を使用します。検討する
f :: Integer -> (Integer, Integer)
f x = let x2 = x * x in (x2, x2+1)
g :: Integer -> Integer
g x = case f x of (y, z) -> y + z
この場合、単純化されたエスケープ分析は(タプルで返されるため)x2
エスケープすると結論付け、ガベージコレクションされたヒープに割り当てる必要がf
ありx2
ます。一方、領域の推論では、戻ってきたx2
ときに割り当てを解除できることを検出できg
ます。ここでの考え方は、のリージョンではなく、のリージョンにx2
割り当てる必要があるということです。g
f
上記のように特定のケースでは地域推論が役立ちますが、遅延評価で効果的に調整するのは難しいようです(エドワードクメットとサイモンペイトンジョーンズのコメントを参照)。たとえば、
f :: Integer -> Integer
f n = product [1..n]
[1..n]
スタックにリストを割り当て、f
戻り後に割り当てを解除したくなるかもしれませんが、これは破滅的ですf
。O(1)メモリ(ガベージコレクションの下)の使用からO(n)メモリに変更されます。
厳密な関数型言語MLの地域推論について、1990年代と2000年代の初めに広範な作業が行われました。Mads Tofte、Lars Birkedal、Martin Elsman、Niels Hallenbergは、リージョンの推論に関する彼らの研究について非常に読みやすい遡及的記事を書き、その多くはMLKitコンパイラーに統合されました。彼らは純粋に領域ベースのメモリ管理(つまり、ガベージコレクターなし)とハイブリッド領域ベース/ガベージコレクションのメモリ管理を実験し、テストプログラムが純粋なガベージよりも「10倍速く、4倍遅い」と実行したと報告しました。収集されたバージョン。
Nothing
)のコピーをの再帰呼び出しに渡しloop
、古いものの割り当てを解除できます-不明なライフタイムはありません。もちろん、大規模なデータ構造ではひどく遅いので、誰もがHaskellの非共有実装を望んでいません。
ささいな例を見てみましょう。これを考えると
f (x, y)
を(x, y)
呼び出す前に、ペアをどこかに割り当てる必要がありますf
。そのペアの割り当てをいつ解除できますか?あなたは何もわかってない。ペアがデータ構造(例:)に配置されてf
いるf
可能性があるため、戻り時に割り当てを解除できませんf p = [p]
。そのため、ペアの存続期間はからの戻りよりも長くする必要がありますf
。さて、ペアがリストに入れられたと言ったら、誰がリストを分解してペアの割り当てを解除できますか?いいえ、ペアが共有されている可能性があるため(例:)let p = (x, y) in (f p, p)
。したがって、ペアの割り当てをいつ解除できるかを判断するのは非常に困難です。
同じことがHaskellのほぼすべての割り当てに当てはまります。とはいえ、寿命に上限を与える分析(領域分析)が可能です。これは、厳密な言語ではある程度うまく機能しますが、遅延言語ではそれほどうまく機能しません(遅延言語は、実装において厳密言語よりもはるかに多くの変更を行う傾向があります)。
だから私は質問を変えたいと思います。HaskellがGCを必要としないのはなぜですか。どのようにメモリ割り当てを行うことを提案しますか?
これが純粋さに関係しているというあなたの直感は、それにいくつかの真実を持っています。
関数の副作用が型シグネチャで説明されるため、Haskellは純粋に考慮されます。したがって、関数に何かを出力するという副作用がある場合、IO
戻り値の型のどこかにある必要があります。
しかし、Haskellのあらゆる場所で暗黙的に使用される関数があり、その型シグネチャが何らかの意味で副作用を説明していません。つまり、一部のデータをコピーして2つのバージョンを戻す機能です。内部的には、これは文字通り、メモリ内のデータを複製することによって、または後で返済する必要がある借金を増やすことによって「実質的に」機能します。
コピー機能を許可しない、さらに制限の厳しい型システム(純粋に「線形」のもの)を使用して言語を設計することは可能です。そのような言語のプログラマーの観点からは、Haskellは少し不純に見えます。
実際、Haskellの親戚であるCleanは線形(厳密には:一意)の型を持っているため、コピーを禁止するのはどのようなものかをある程度理解できます。ただし、Cleanでは、「非一意」タイプのコピーは引き続き許可されます。
この領域には多くの研究があり、Googleを十分に活用すれば、ガベージコレクションを必要としない純粋な線形コードの例を見つけることができます。どのメモリが使用されるかをコンパイラに通知できるあらゆる種類のタイプシステムが見つかり、コンパイラがGCの一部を削除できるようになります。
量子アルゴリズムも純粋に線形であるという感覚があります。すべての操作は元に戻せるため、データの作成、コピーはできません、または破棄。(通常の数学的な意味でも線形です。)
重複が発生したときに明確になるDUP操作が明示されているForth(または他のスタックベースの言語)と比較することも興味深いです。
これについての別の(より抽象的な)考え方は、Haskellがデカルト閉カテゴリの理論に基づく単純に型付けされたラムダ計算から構築され、そのようなカテゴリには対角関数が備わっていることに注意することdiag :: X -> (X, X)
です。カテゴリの別のクラスに基づく言語には、そのようなものがない場合があります。
しかし、一般に、純粋な線形プログラミングは実用的であるには難しすぎるため、GCを使用します。
Haskellに適用される標準実装手法では、以前の値を変更することはなく、代わりに以前の値に基づいて新しい変更された値を作成するため、他のほとんどの言語よりも実際にGCが必要です。これは、プログラムが常により多くのメモリを割り当てて使用していることを意味するため、時間が経過すると、多数の値が破棄されます。
これが、GHCプログラムが(ギガバイトからテラバイトまで)非常に高い合計割り当て数を持つ傾向がある理由です。それらは常にメモリを割り当てており、実行前にメモリを回収するのは効率的なGCのおかげです。
言語(任意の言語)でオブジェクトを動的に割り当てることができる場合、メモリの管理に対処するための3つの実用的な方法があります。
この言語では、スタック上または起動時にのみメモリを割り当てることができます。ただし、これらの制限により、プログラムが実行できる計算の種類が大幅に制限されます。(実際には、動的データ構造を(たとえば)Fortranで大きな配列で表すことにより、エミュレートできます。これはHORRIBLE ...であり、この説明には関係ありません。)
言語は明示的free
またはdispose
メカニズムを提供できます。しかし、これはプログラマがそれを正しく行うことに依存しています。ストレージ管理に誤りがあると、メモリリークが発生する可能性があります。
言語(より厳密には、言語の実装)は、動的に割り当てられたストレージに自動ストレージマネージャーを提供できます。つまり、何らかの形のガベージコレクタです。
他の唯一のオプションは、動的に割り当てられたストレージを決して再利用しないことです。これは、小さな計算を実行する小さなプログラムを除いて、実際的な解決策ではありません。
これをHaskellに適用すると、言語には1の制限がなく、2のように手動で割り当て解除操作は行われません。したがって、重要なことに使用できるようにするために、Haskell実装にはガベージコレクターを含める必要があります。
純粋な言語でGCが必要になるケースは考えられません。
おそらくあなたは純粋な関数型言語を意味します。
答えは、言語が作成しなければならないヒープオブジェクトを取り戻すには、内部でGCが必要であることです。例えば。
純粋な関数は、場合によってはヒープオブジェクトを返す必要があるため、ヒープオブジェクトを作成する必要があります。つまり、スタックに割り当てることができません。
サイクルが存在する可能性があるという事実(let rec
たとえばに起因)は、参照カウントアプローチがヒープオブジェクトに対して機能しないことを意味します。
次に、関数クロージャーがあります。これは、それらが作成されたスタックフレームから(通常)独立している存続期間を持っているため、スタックに割り当てることもできません。
GCが存在しない場合にリークするコード例を探しています。
クロージャーやグラフ状のデータ構造を含むあらゆる例は、これらの状況下でリークします。
十分なメモリがあれば、ガベージコレクターは必要ありません。ただし、実際には無限のメモリはないため、不要になったメモリを解放するための方法が必要です。Cのような不純な言語では、メモリを解放して解放することを明示的に指定できますが、これは変更操作であるため(解放したばかりのメモリは安全に読み取ることはできません)、この方法は使用できません。純粋な言語。したがって、メモリを解放できる場所(一般的なケースではおそらく不可能)を静的に分析するか、ふるいのようにメモリをリークする(使い果たされるまで機能する)か、GCを使用します。
Haskellは厳密ではないプログラミング言語ですが、ほとんどの実装は呼び出しごと(怠惰)を使用して厳密でないプログラミングを実装しています。call-by-needでは、「サンク」(評価されるのを待ってから自分自身を上書きし、必要に応じて値を再利用できるように見える状態を維持する式)を使用して、実行時に到達したときにのみ評価します。
したがって、サンクを使用して遅延して言語を実装する場合、オブジェクトの存続期間に関するすべての推論は最後の瞬間(実行時)まで延期されています。寿命について何も知らないので、合理的にできる唯一のことはガベージコレクトです...