Haskellはガベージコレクターを必要としますか?


118

Haskellの実装がなぜGCを使用するのか、私は興味があります。

純粋な言語でGCが必要になるケースは考えられません。コピーを減らすための最適化だけですか、それとも実際に必要ですか?

GCが存在しない場合にリークするコード例を探しています。


14
このシリーズは啓発的であるかもしれません。ガベージがどのように生成(およびその後に収集)されるかについて説明します。blog.ezyang.com
トムクロケット

5
純粋な言語でどこにでも参照があります!単に変更可能な参照ではありません。
トムクロケット

1
@pelotom不変データまたは不変参照への参照?
Pubby 2012年

3
両方とも。参照されているデータが不変であるという事実は、すべての参照がずっと不変であるという事実から来ています。
トムクロケット

4
この理由付けをメモリ割り当てに適用すると、一般的な場合に割り当て解除を静的に予測できない理由を理解するのに役立つため、停止問題に確実に関心を持つことになります。しかし、ある一部の解除は、彼らがしているだけのように、予測することができたため、プログラムいくつか実際にそれらを実行せずに終了するように知ることができるプログラムが。
Paul R

回答:


218

他の人がすでに指摘したように、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時に安全に割り当てを解除できることを検出することを期待するかもしれません。基本的に、コンパイラーがエスケープ分析を実行して、割り当てをガベージコレクションされたヒープに変換し、可能な限りスタック上の割り当てに変換するように求めています。fx2

これは無理ではありません。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割り当てる必要があるということです。gf

ハスケルを超えて

上記のように特定のケースでは地域推論が役立ちますが、遅延評価で効果的に調整するのは難しいようです(エドワードクメットサイモンペイトンジョーンズのコメントを参照)。たとえば、

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倍遅い」と実行したと報告しました。収集されたバージョン。


2
Haskellは共有を必要としますか?そうでない場合、最初の例では、リスト(またはNothing)のコピーをの再帰呼び出しに渡しloop、古いものの割り当てを解除できます-不明なライフタイムはありません。もちろん、大規模なデータ構造ではひどく遅いので、誰もがHaskellの非共有実装を望んでいません。
nimi

3
私の唯一の混乱は最初の例にありますが、私はこの答えが本当に好きです。明らかにユーザーが「クリア」と入力しなかった場合は、無限のメモリ(GCなし)を使用できますが、メモリは引き続き追跡されているため、これは厳密にはリークではありません。
Pubby

3
C ++ 11には、スマートポインターのすばらしい実装があります。基本的に、参照カウントを採用しています。Haskellは、ガベージコレクションを破棄して、似たようなものを優先させ、決定論的になる可能性があると思います。
intrepidis 2013年

3
@ChrisNash-機能しません。スマートポインターは、内部で参照カウントを使用します。参照カウントは、循環のあるデータ構造を処理できません。Haskellは、サイクルを持つデータ構造を生成できます。
スティーブンC

3
この回答の動的メモリ割り当ての部分に同意するかどうかはわかりません。プログラムがユーザーが一時的にループを停止するタイミングを知らないからといって、動的にすべきではありません。それは、何かがコンテキストから外れるかどうかコンパイラが知っているかどうかによって決まります。言語文法自体によって正式に定義されているHaskellの場合、人生のコンテキストは既知です。ただし、リスト式と型が言語内で動的に生成されるため、メモリは依然として動的である可能性があります。
ティモシースワン

27

ささいな例を見てみましょう。これを考えると

f (x, y)

(x, y)呼び出す前に、ペアをどこかに割り当てる必要がありますf。そのペアの割り当てをいつ解除できますか?あなたは何もわかってない。ペアがデータ構造(例:)に配置されてfいるf可能性があるため、戻り時に割り当てを解除できませんf p = [p]。そのため、ペアの存続期間はからの戻りよりも長くする必要がありますf。さて、ペアがリストに入れられたと言ったら、誰がリストを分解してペアの割り当てを解除できますか?いいえ、ペアが共有されている可能性があるため(例:)let p = (x, y) in (f p, p)。したがって、ペアの割り当てをいつ解除できるかを判断するのは非常に困難です。

同じことがHaskellのほぼすべての割り当てに当てはまります。とはいえ、寿命に上限を与える分析(領域分析)が可能です。これは、厳密な言語ではある程度うまく機能しますが、遅延言語ではそれほどうまく機能しません(遅延言語は、実装において厳密言語よりもはるかに多くの変更を行う傾向があります)。

だから私は質問を変えたいと思います。HaskellがGCを必要としないのはなぜですか。どのようにメモリ割り当てを行うことを提案しますか?


18

これが純粋さに関係しているというあなたの直感は、それにいくつかの真実を持っています。

関数の副作用が型シグネチャで説明されるため、Haskellは純粋に考慮されます。したがって、関数に何かを出力するという副作用がある場合、IO戻り値の型のどこかにある必要があります。

しかし、Haskellのあらゆる場所で暗黙的に使用される関数があり、その型シグネチャが何らかの意味で副作用を説明していません。つまり、一部のデータをコピーして2つのバージョンを戻す機能です。内部的には、これは文字通り、メモリ内のデータを複製することによって、または後で返済する必要がある借金を増やすことによって「実質的に」機能します。

コピー機能を許可しない、さらに制限の厳しい型システム(純粋に「線形」のもの)を使用して言語を設計することは可能です。そのような言語のプログラマーの観点からは、Haskellは少し不純に見えます。

実際、Haskellの親戚であるCleanは線形(厳密には:一意)の型を持っているため、コピーを禁止するのはどのようなものかをある程度理解できます。ただし、Cleanでは、「非一意」タイプのコピーは引き続き許可されます。

この領域には多くの研究があり、Googleを十分に活用すれば、ガベージコレクションを必要としない純粋な線形コードの例を見つけることができます。どのメモリが使用されるかをコンパイラに通知できるあらゆる種類のタイプシステムが見つかり、コンパイラがGCの一部を削除できるようになります。

量子アルゴリズムも純粋に線形であるという感覚があります。すべての操作は元に戻せるため、データの作成、コピーはできません、または破棄。(通常の数学的な意味でも線形です。)

重複が発生したときに明確になるDUP操作が明示されているForth(または他のスタックベースの言語)と比較することも興味深いです。

これについての別の(より抽象的な)考え方は、Haskellがデカルト閉カテゴリの理論に基づく単純に型付けされたラムダ計算から構築され、そのようなカテゴリには対角関数が備わっていることに注意することdiag :: X -> (X, X)です。カテゴリの別のクラスに基づく言語には、そのようなものがない場合があります。

しかし、一般に、純粋な線形プログラミングは実用的であるには難しすぎるため、GCを使用します。


3
私がこの回答を書いて以来、Rustプログラミング言語の人気はかなり上昇しています。したがって、Rustがメモリへのアクセスを制御するために線形っぽいタイプのシステムを使用していることは言及する価値があり、私が言及したアイデアが実際に使用されているのを確認したい場合は、一見の価値があります。
sigfpe 2016

14

Haskellに適用される標準実装手法では、以前の値を変更することはなく、代わりに以前の値に基づいて新しい変更された値を作成するため、他のほとんどの言語よりも実際にGCが必要です。これは、プログラムが常により多くのメモリを割り当てて使用していることを意味するため、時間が経過すると、多数の値が破棄されます。

これが、GHCプログラムが(ギガバイトからテラバイトまで)非常に高い合計割り当て数を持つ傾向がある理由です。それらは常にメモリを割り当てており、実行前にメモリを回収するのは効率的なGCのおかげです。


2
「以前の値を変更することはありません」:haskell.org/haskellwiki/HaskellImplementorsWorkshop/2011/Takanoを確認できます。これは、メモリを再利用する実験的なGHC拡張に関するものです。
gfour 2012年

11

言語(任意の言語)でオブジェクトを動的に割り当てることができる場合、メモリの管理に対処するための3つの実用的な方法があります。

  1. この言語では、スタック上または起動時にのみメモリを割り当てることができます。ただし、これらの制限により、プログラムが実行できる計算の種類が大幅に制限されます。(実際には、動的データ構造を(たとえば)Fortranで大きな配列で表すことにより、エミュレートできます。これはHORRIBLE ...であり、この説明には関係ありません。)

  2. 言語は明示的freeまたはdisposeメカニズムを提供できます。しかし、これはプログラマがそれを正しく行うことに依存しています。ストレージ管理に誤りがあると、メモリリークが発生する可能性があります。

  3. 言語(より厳密には、言語の実装)は、動的に割り当てられたストレージに自動ストレージマネージャーを提供できます。つまり、何らかの形のガベージコレクタです。

他の唯一のオプションは、動的に割り当てられたストレージを決して再利用しないことです。これは、小さな計算を実行する小さなプログラムを除いて、実際的な解決策ではありません。

これをHaskellに適用すると、言語には1の制限がなく、2のように手動で割り当て解除操作は行われません。したがって、重要なことに使用できるようにするために、Haskell実装にはガベージコレクターを含める必要があります。

純粋な言語でGCが必要になるケースは考えられません。

おそらくあなたは純粋な関数型言語を意味します。

答えは、言語が作成しなければならないヒープオブジェクトを取り戻すには、内部でGCが必要であることです。例えば。

  • 純粋な関数は、場合によってはヒープオブジェクトを返す必要があるため、ヒープオブジェクトを作成する必要があります。つまり、スタックに割り当てることができません。

  • サイクルが存在する可能性があるという事実(let recたとえばに起因)は、参照カウントアプローチがヒープオブジェクトに対して機能しないことを意味します。

  • 次に、関数クロージャーがあります。これは、それらが作成されたスタックフレームから(通常)独立している存続期間を持っているため、スタックに割り当てることもできません。

GCが存在しない場合にリークするコード例を探しています。

クロージャーやグラフ状のデータ構造を含むあらゆる例は、これらの状況下でリークします。


2
なぜオプションのリストが網羅的だと思いますか?Objective CでのARC、MLKitとDDCでの領域の推論、Mercuryでのコンパイル時のガベージコレクション-これらはすべてこのリストに適合しません。
Dee Mon

@DeeMon-それらはすべてこれらのカテゴリの1つに適合します。あなたがそうではないと思うなら、それはあなたがカテゴリーの境界をきつすぎて描いているからです。「何らかの形のガベージコレクション」とは、ストレージが自動的に回収されるメカニズムを意味します。
Stephen C

1
C ++ 11はスマートポインターを使用します。基本的に、参照カウントを採用しています。確定的で自動です。Haskellの実装がこのメソッドを使用するのを見たいです。
intrepidis 2013年

2
@ChrisNash-1)うまくいきません。参照カウントベースの再利用は、サイクルがあるとデータをリークします...アプリケーションコードに依存してサイクルを壊すことができない場合を除きます。2)最近の(実際の)ガベージコレクターと比較した場合、参照カウントのパフォーマンスが低いことは(これらのことを研究している人には)よく知られています。
スティーブンC

@DeeMon-さらに、地域推論がHaskellで実用的ではない理由についてのReinerpの回答を参照してください。
スティーブンC

8

十分なメモリがあれば、ガベージコレクターは必要ありません。ただし、実際には無限のメモリはないため、不要になったメモリを解放するための方法が必要です。Cのような不純な言語では、メモリを解放して解放することを明示的に指定できますが、これは変更操作であるため(解放したばかりのメモリは安全に読み取ることはできません)、この方法は使用できません。純粋な言語。したがって、メモリを解放できる場所(一般的なケースではおそらく不可能)を静的に分析するか、ふるいのようにメモリをリークする(使い果たされるまで機能する)か、GCを使用します。


これは、GCが一般的に不要である理由を答えますが、私は特にHaskellに興味があります。
Pubby 2012年

10
GCが一般的に理論的に不要である場合、Haskellにとっても理論的に不要であることは自明です。
2012年

@ 必要なことを言うつもりだった、私のスペルチェッカーが意味を裏返したと思う。
Pubby 2012年

1
Ehirdコメントはまだ保持しています:-)
ポールR

2

GCは、純粋なFP言語では「必須」です。どうして?allocとfreeの操作は不純です!そして2番目の理由は、不変の再帰的データ構造は存在のためにGCを必要とするということです。もちろん、それを使用する構造のコピーは非常に安価であるため、バックリンクは恵みです。

とにかく、あなたが私を信じていない場合は、FP言語を実装してみてください。そうすれば、私が正しいとわかります。

編集:忘れました。怠惰はGCのない地獄です。信じられない?たとえば、C ++では、GCなしで試してください。あなたが見るでしょう...物事


1

Haskellは厳密ではないプログラミング言語ですが、ほとんどの実装は呼び出しごと(怠惰)を使用して厳密でないプログラミングを実装しています。call-by-needでは、「サンク」(評価されるのを待ってから自分自身を上書きし、必要に応じて値を再利用できるように見える状態を維持する式)を使用して、実行時に到達したときにのみ評価します。

したがって、サンクを使用して遅延して言語を実装する場合、オブジェクトの存続期間に関するすべての推論は最後の瞬間(実行時)まで延期されています。寿命について何も知らないので、合理的にできる唯一のことはガベージコレクトです...


1
場合によっては、静的分析がこれらのサンクコードに挿入され、サンクの評価後に一部のデータが解放されます。割り当て解除は実行時に行われますが、GCではありません。これは、C ++でのスマートポインターの参照カウントのアイデアに似ています。オブジェクトのライフタイムに関する推論はランタイムで発生しますが、GCは使用されません。
Dee Mon
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.