ガベージコレクションは、ネイティブにコンパイルされた言語でどのように機能しますか?


79

スタックオーバーフローに関するいくつかの回答を参照した後、ネイティブコンパイルされた言語の一部にガベージコレクションがあることが明らかです。しかし、これが正確にどのように機能するかは私には不明です。

ガベージコレクションがインタプリタ言語でどのように機能するかを理解しています。ガベージコレクターは、単にインタープリターと一緒に実行され、プログラムのメモリから未使用および到達不能なオブジェクトを削除します。両方が一緒に実行されています。

しかし、これはコンパイルされた言語でどのように機能しますか?私の理解では、コンパイラがソースコードをターゲットコード(特にネイティブマシンコード)にコンパイルすると、完了します。その仕事は終わりました。それでは、コンパイルされたプログラムをどのようにガベージコレクションするのでしょうか?

「ガベージ」オブジェクトを削除するためにプログラムが実行されている間、コンパイラは何らかの方法でCPUと連携しますか?または、コンパイラは、コンパイルされたプログラムの実行可能ファイルに最小限のガベージコレクタを含めますか。

Stack Overflowに関するこの回答からの抜粋により、後者のステートメントは前者よりも妥当性があると思います

そのようなプログラミング言語の1つがEiffelです。ほとんどのEiffelコンパイラは、移植性の理由でCコードを生成します。このCコードは、標準Cコンパイラによってマシンコードを生成するために使用されます。Eiffel実装は、このコンパイルされたコードに対してGC(および場合によっては正確なGC)を提供します。VMは必要ありません。特に、VisualEiffelコンパイラーはネイティブのx86マシンコードをフルGCサポートで直接生成しました

最後のステートメントは、プログラムの実行中にガベージコレクターとして機能するプログラムを最終的な実行可能ファイルにコンパイラが含めることを意味するようです。

ネイティブにコンパイルされ、オプションのガベージコレクタを備えたガベージコレクションに関するD言語のWebサイトのページも、ガベージコレクションを実装するために元の実行可能プログラムと一緒にバックグラウンドプログラムが実行されることを示唆しているようです。

Dは、ガベージコレクションをサポートするシステムプログラミング言語です。通常、メモリを明示的に解放する必要はありません。必要に応じて割り当てるだけで、ガベージコレクターはすべての未使用メモリを利用可能なメモリのプールに定期的に返します。

この方法は、上記の場合はされて使用され、どのように正確にそれが働くだろうか?コンパイラは、ガベージコレクションプログラムのコピーを保存し、生成する各実行可能ファイルに貼り付けますか?

または、私は私の考えに欠陥がありますか?その場合、コンパイルされた言語のガベージコレクションを実装するためにどのメソッドが使用され、どのように正確に機能しますか


1
この質問の近い投票者が正確に何が間違っているかを述べることができれば、私はそれを修正することができれば感謝しますか?
クリスチャンディーン

6
GCが基本的に特定のプログラミング言語の実装に必要なライブラリの一部であるという事実を受け入れた場合、質問の要旨はGC自体とは関係なく、静的リンクと動的リンクに関係します。
セオドロスチャツィジアンナキス

7
ガベージコレクタは、に相当する言語を実装するランタイムライブラリの一部であると考えることができますmalloc()
バーマー

9
ガベージコレクターの動作は、コンパイルモデルではなく、アロケーターの特性に依存します。アロケーターは、割り当てられたすべてのオブジェクトを知っています。それらを割り当てました。必要なのは、どのオブジェクトがまだ生きているかを知る何らかの方法であり、コレクターはそれらを除くすべてのオブジェクトの割り当てを解除できます。その説明には、コンパイルモデルとは何の関係もありません。
エリックリッパー

1
GCは動的メモリの機能であり、インタープリターの機能ではありません。
ドミトリーグリゴリエフ

回答:


52

コンパイルされた言語のガベージコレクションは、インタプリタ言語の場合と同じように機能します。Goのような言語は、通常コードが事前にマシンコードにコンパイルされている場合でも、トレースガベージコレクターを使用します。

(トレース)ガベージコレクションは通常、現在実行中のすべてのスレッドの呼び出しスタックをウォークすることから始まります。それらのスタック上のオブジェクトは常にライブです。その後、ガベージコレクターは、ライブオブジェクトグラフ全体が検出されるまで、ライブオブジェクトが指すすべてのオブジェクトを走査します。

これを行うには、Cなどの言語が提供しない追加情報が必要であることは明らかです。特に、すべてのポインター(およびおそらくそのデータ型)のオフセットを含む各関数のスタックフレームのマップ、および同じ情報を含むすべてのオブジェクトレイアウトのマップが必要です。

ただし、型保証が強い言語(たとえば、異なるデータ型へのポインターキャストが許可されていない場合)が、コンパイル時にそれらのマップを実際に計算できることは簡単にわかります。バイナリ内の命令アドレスとスタックフレームマップ間の関連付け、およびデータ型とオブジェクトレイアウトマップ間の関連付けを単に保存します。その後、この情報により、オブジェクトグラフトラバーサルを実行できます。

ガベージコレクター自体は、C標準ライブラリと同様に、プログラムにリンクされたライブラリにすぎません。たとえば、このライブラリはmalloc()、メモリの負荷が高い場合に収集アルゴリズムを実行するのと同様の機能を提供できます。


9
ユーティリティライブラリとJITコンパイルの間に、「ネイティブにコンパイルされる」と「ランタイム環境で実行する」の境界線がますます曖昧になっています。
corsiKa

6
GCサポートが付属していない言語について少しだけ追加します。Cなどの言語は呼び出しスタックに関する情報を提供しませんが、プラットフォーム固有のコード(通常は少し含めて) (アセンブリコードの))「保守的なガベージコレクション」を実装することはまだ可能です。ベームGCは、実際のプログラムで使用されるこの一例です。
マッティバークネン

2
@corsiKaむしろ、線ははるかに明確です。今、私たちはそれらが異なる無関係な概念であり、お互いの反意語ではないことがわかります。
Kroltan

4
コンパイル済みランタイムと解釈済みランタイムで注意する必要がある追加の複雑さの1つは、「(トレース)ガベージコレクションは通常、現在実行中のすべてのスレッドの呼び出しスタックを調べることから始まります。」コンパイル済み環境でGCを実装した私の経験では、スタックをトレースするだけでは不十分です。通常、開始点はそれらのレジスターからトレースするのに十分な長さのスレッドを一時停止することです。なぜなら、それらのレジスターには、まだスタックに格納されていない参照が含まれているからです。通訳の場合、これは通常...ではありません
ジュール・

...問題は、インタプリタがすべてのデータが解釈されたスタックに安全に保存されていることを知っている「安全なポイント」でGCが実行されるように環境が調整できるためです。
ジュール

123

コンパイラは、ガベージコレクションプログラムのコピーを保存し、生成する各実行可能ファイルに貼り付けますか?

違法で奇妙に聞こえますが、はい。コンパイラには、ガベージコレクションコードだけでなく、ユーティリティライブラリ全体が含まれており、このライブラリへの呼び出しは、作成する各実行可能ファイルに挿入されます。これは実行時ライブラリと呼ばれ、通常それが提供するさまざまなタスクの数に驚くでしょう。


51
@ChristianDean Cにもランタイムライブラリがあることに注意してください。それはGCを持っていませんが、それはまだそのランタイムライブラリを介してメモリ管理を行いますmalloc()し、free()言語に組み込まれていないが、オペレーティングシステムの一部ではありませんが、このライブラリの関数です。C ++は、GCを念頭に置いて設計された言語ではありませんが、ガベージコレクションライブラリを使用してコンパイルされることもあります。
アモン

18
C ++にはdynamic_cast、GCを追加しなくても、メイクや例外の動作などを実行するランタイムライブラリも含まれています。
セバスチャンレッド

23
実行時ライブラリは必ずしも各実行可能ファイルにコピーされるわけではなく(静的リンクと呼ばれます)、実行時にのみ参照(ライブラリを含むバイナリへのパス)してアクセスできます。これは動的リンクです。
-mouviciel

16
コンパイラは、他に何も起こらずにプログラムのエントリポイントに直接ジャンプする必要もありません。私は、すべてのコンパイラーがを呼び出す前に実際に多くのプラットフォーム固有の初期化コードを挿入するという経験に基づいた推測を行っていmain()ます。(GCはメモリ割り当て呼び出し内で行われないと仮定します。)実行時に、GCはオブジェクトのどの部分がポインターまたはオブジェクト参照であるかを実際に知る必要があり、コンパイラーはオブジェクト参照をポインターに変換するコードを発行する必要がありますGCがオブジェクトを再配置する場合。
ミリムース

15
@millimoose:はい。たとえば、GCCでは、このコードはcrt0.o( " C R un T ime、the very basics"を表します)、すべてのプログラム(または少なくとも独立していないすべてのプログラム)にリンクされます。
ヨルグWミットタグ

58

または、コンパイラはコンパイルされたプログラムのコードに最小限のガベージコレクターを含めますか。

これは、「コンパイラがプログラムをガベージコレクションを実行するライブラリにリンクする」という奇妙な言い方です。しかし、はい、それが起こっています。

これは特別なことではありません。コンパイラは通常、大量のライブラリをコンパイルするプログラムにリンクします。それ以外の場合、コンパイルされたプログラムは、多くのことをゼロから再実装しない限り、あまり実行できません。テキストを画面/ファイル/に書き込むことさえ、ライブラリを必要とします。

しかし、GCは、ユーザーが呼び出す明示的なAPIを提供するこれらの他のライブラリとは異なるのでしょうか?

いいえ:ほとんどの言語では、ランタイムライブラリは、GCを超えて、公開APIなしで多くの舞台裏作業を行います。次の3つの例を検討してください。

  1. 例外の伝播とスタックのアンワインド/デストラクタの呼び出し。
  2. 動的メモリ割り当て(通常は、ガベージコレクションがない場合でも、Cのように関数を呼び出すだけではありません)。
  3. 動的な型情報の追跡(キャストなど)。

したがって、ガベージコレクションライブラリはまったく特別なものではなく、プログラムが事前にコンパイルされたかどうかにはアプリオリは関係ありません。


これは作られたポイントを超える大幅な何かを提供しているようだと説明していないトップの答えの前に3時間を掲示
ブヨ

11
@gnat私は、トップの答えが十分に強くないので、それが有用/必要だと感じました:それは同様の事実に言及していますが、ガベージコレクションのシングルアウトは完全に人為的な区別であると指摘することはできません。基本的に、OPの仮定には欠陥があり、トップアンサーはこれについて言及していません。私はそうします(「欠陥」というやや粗野な用語を避けます)。
コンラッドルドルフ

それほど特別なことではありませんが、通常、人々はライブラリを自分のコードから明示的に呼び出すものと考えるので、やや特別だと思います。基本的な言語セマンティクスの実装ではありません。ここでのOPの間違った仮定は、コンパイラが作成者が指定していないライブラリ呼び出しでインストルメントするのではなく、多少なりとも簡単な方法でコードを変換するだけだということです。
ミリムース

7
@millimooseランタイムライブラリは、明示的なユーザー操作なしで、さまざまな方法でバックグラウンドで動作します。例外の伝播とスタックのアンワインド/デストラクタの呼び出しを検討してください。動的なメモリ割り当てを検討してください(ガベージコレクションがない場合でも、通常はCのように関数を呼び出すだけではありません)。動的な型情報の処理を検討してください(キャストなど)。したがって、GCは本当にユニークではありません。
コンラッドルドルフ

3
はい、私はそれを奇妙に言いました。それは、コンパイラが実際にそのようなことを実際に行っていることに懐疑的だったからです。しかし、今私はそれについて考えると、それははるかに理にかなっています。コンパイラは、標準ライブラリの他の部分のようにガベージコレクタを単純にリンクできます。私の混乱の一部は、ガベージコレクターをインタープリターの実装の一部にすぎず、それ自体が別個のプログラムではないと考えていたことに起因すると考えています。
クリスチャンディーン

23

しかし、これはコンパイルされた言語でどのように機能しますか?

あなたの言葉遣いは間違っています。プログラミング言語はある仕様、いくつかの技術的な報告書に書かれた(良い例えば、参照R5RSを)。実際には、特定の言語実装(ソフトウェア)を参照しています。

(一部のプログラミング言語は仕様が間違っているか、欠落している場合や、サンプル実装に準拠している場合があります。それでも、プログラミング言語は動作を定義します -たとえば、構文セマンティクスを持ちます - ソフトウェア製品ではありませんが、いくつかのソフトウェア製品によって実装されます;多くのプログラミング言語にはいくつかの実装があります;特に、「コンパイルされた」は実装に適用される形容詞です-たとえプログラミング言語がコンパイラよりもインタープリターによって実装されやすい場合でも)

私の理解では、コンパイラがソースコードをターゲットコード、特にネイティブマシンコードにコンパイルすると、完了します。その仕事は終わりました。

インタープリターとコンパイラーは大まかな意味を持ち、一部の言語実装は両方であると見なされることに注意してください。つまり、間に連続性があります。最新読むドラゴンブックと考えるバイトコードJITコンパイル動的にコンパイルされたCコードを放出するいくつかの後、「プラグイン」のdlopen(3)同じプロセスで-ed(および現在のマシンでは、これと互換性があるために十分に高速でありますインタラクティブREPLこれを参照)


GCハンドブックを読むことを強くお勧めします。答えるには本全体が必要です。その前に、ガベージコレクション wikiページを読んでください(以下を読む前に読んでいると思います)。

コンパイルされた言語実装のランタイムシステムにはガベージコレクターが含まれており、コンパイラはその特定のランタイムシステムに適合するコードを生成しています。特に、割り当てプリミティブ(マシンコードにコンパイルされる)は、ランタイムシステムを呼び出します(または呼び出します)。

それでは、コンパイルされたプログラムをどのようにガベージコレクションするのでしょうか?

ランタイムシステムを使用する(そして「フレンドリー」で「互換性がある」)マシンコードを出力するだけです。

あなたは特に、いくつかのガベージコレクションライブラリを見つけることができることに注意してくださいベームGCRavenbrookのMPS、あるいは私の(メンテナンスされていない)Qish。また、単純な GCのコーディングはそれほど難しくありません(ただし、デバッグは難しく、競合する GCのコーディングは困難です)。

場合によっては、コンパイラは保守的な GC(Boehm GCなど)を使用します。次に、コーディングすることはあまりありません。保守的なGCは(コンパイラがその割り当てルーチンまたはGCルーチン全体を呼び出すとき)コールスタック全体をスキャンし、コールスタックから(間接的に)アクセス可能なメモリゾーンがライブであると想定します。入力情報が失われるため、これは保守的な GC と呼ばれます。呼び出しスタック上の整数が偶然アドレスのように見える場合は、追跡されます。

他の(より困難な)場合、ランタイムは世代別コピーガベージコレクションを提供します(典型的な例は、Ocamlコンパイラーで、このようなGCを使用してOcamlコードをマシンコードにコンパイルします)。次に、問題は呼び出しスタックですべてのポインターを正確に見つけることであり、それらのいくつかはGCによって移動されます。次に、コンパイラーは、ランタイムが使用する呼び出しスタックフレームを記述するメタデータを生成します。そのため、呼び出し規約ABIは、その実装(つまりコンパイラ)とランタイムシステムに固有のものになりつつあります。

場合によっては、コンパイラーによって生成されたマシンコード(実際にはそれを指すクロージャーですら)自体がガベージコレクトされます。これは特に、すべてのREPLインタラクションに対してマシンコードを生成するSBCL(優れたCommon Lisp実装)の場合です。これには、コードとその内部で使用される呼び出しフレームを記述するメタデータも必要です。

コンパイラは、ガベージコレクションプログラムのコピーを保存し、生成する各実行可能ファイルに貼り付けますか?

並べ替え。しかし、ランタイムシステムは、時々 、(Linuxや他のいくつかのPOSIXシステムでは)それもに渡されたスクリプトインタプリタ、例えば可能性など、共有ライブラリ可能性(2)はexecveシェバング。または、ELFインタープリター。elf(5)およびPT_INTERPなどを参照してください。

ところで、ガベージコレクション(およびそのランタイムシステム)を使用する言語のほとんどのコンパイラは、今日ではフリーソフトウェアです。したがって、ソースコードをダウンロードして調べてください。


5
あなたは、明示的な仕様のない多くのプログラミング言語の実装があることを意味します。はい、私はそれに同意します。しかし、私のポイントは、プログラミング言語はソフトウェアではないということです(コンパイラーやインタープリターなど)。これは、構文とセマンティクスを備えたものです(おそらく両方とも不明確です)。
バジルスタリンケビッチ

4
@KonradRudolph:「フォーマル」と「仕様」の定義に完全に依存します:-D ISO 1.8 / 1.9の共通部分の小さなサブセットを指定するISO / IEC 30170:2012 Ruby Programming Language Specificationがあります。あるRubyのスペックスイート、「実行可能な仕様」の一種として機能境界例例のセットが。次に、David FlanaganとYukihiro MatsumotoによるRubyプログラミング言語
ヨルグWミットタグ

4
また、The Ruby DocumentationRuby Issue Trackerでの問題の議論。ruby-core(英語)およびruby-dev(日本語)メーリングリストに関する議論。コミュニティの常識的な期待(たとえばArray#[]、O(1)最悪の場合、Hash#[]O(1)償却された最悪の場合)。最後になりましたが、マッツの脳です。
ヨルグWミットタグ

6
@KonradRudolph:ポイントは、正式な仕様がなく、単一の要素しか使用されていない言語でさえ、「言語」(抽象的な規則と制限)と「実装」(これらの規則に従ったコード処理プログラムと制限)。そして、実装は、些細なものではありますが、それでも仕様を生み出します。つまり、「コードが行うことはすべて仕様です」。これが、ISO仕様、RubySpec、およびRDocの作成方法です。
ヨルグWミットタグ

1
Bohemのガベージコレクターをご紹介いただきありがとうございます。OPは、既存のコンパイラに「ボルトオン」された場合でも、ガベージコレクションがいかに単純であるかの優れた例であるため、OPを検討することをお勧めします。
コートアンモン

6

すでにいくつかの良い答えがありますが、この質問の背後にあるいくつかの誤解を解消したいと思います。

それ自体は「ネイティブにコンパイルされた言語」のようなものはありません。たとえば、同じJavaコードが古い電話(Java Dalvik)で解釈され(実行時に部分的にジャストインタイムでコンパイルされ)、新しい電話(ART)で(前もって)コンパイルされます。

コードをネイティブで実行することと解釈することの違いは、見かけよりもはるかに厳密ではありません。両方とも、動作するためにいくつかのランタイムライブラリとオペレーティングシステムが必要です(*)。インタープリターコードにはインタープリターが必要ですが、インタープリターはランタイムの一部にすぎません。ただし、インタープリターを(ジャストインタイム)コンパイラーに置き換えることができるため、これでも厳密ではありません。最大のパフォーマンスを得るには、両方が必要な場合があります(デスクトップJavaランタイムには、インタープリターと2つのコンパイラーが含まれています)。

どのようにコードを実行しても、同じように動作するはずです。メモリの割り当てと解放は、ランタイムのタスクです(ファイルを開く、スレッドを開始するなど)。あなたの言語で、あなたはただ書くnew X()か似ています。言語仕様には何が起こるべきかが記述されており、ランタイムがそれを行います。

空きメモリの一部が割り当てられたり、コンストラクタが呼び出されたりします。十分なメモリがない場合、ガベージコレクタが呼び出されます。既にコードのネイティブな部分であるランタイムにいるので、インタープリターの存在はまったく問題ではありません。

コードの解釈とガベージコレクションの間には、実際には直接的な関係はありません。Cのような低レベル言語は、すべての速度ときめ細かな制御のために設計されているだけであり、非ネイティブコードやガベージコレクターの考えにはうまく合いません。したがって、相関関係があります。

これは、たとえばJavaインタープリターが非常に遅く、ガベージコレクターがかなり非効率的だった昔、非常に真実でした。今日、物事は大きく異なり、インタープリター言語について話すことは意味を失っています。


(*)少なくとも汎用コードについては、ブートローダーなどを残してください。


OcamlとSBCLはどちらもネイティブコンパイラです。そのため「ネイティブコンパイル言語」の実装があります。
バジルスタリンケビッチ

@BasileStarynkevitch WAT?あまり知られていないコンパイラの命名は私の答えにどのように関係していますか?最初に解釈された言語のコンパイラとしてのSBCLは、区別が意味をなさないという私の主張を支持する議論ではありませんか?
-maaartinus

Common Lisp(またはその他の言語)は解釈またはコンパイルされません。これはプログラミング言語(仕様)です。その実装は、コンパイラ、インタプリタ、またはその中間のもの(バイトコードインタプリタなど)です。SBCLは、Common Lispのインタラクティブなコンパイルされた実装です。Ocamlはプログラミング言語でもあります(実装としてバイトコードインタープリターとネイティブコンパイラの両方を使用)。
バジルスタリンケビッチ

@BasileStarynkevitchそれは私が主張していることです。1.インタプリタ言語またはコンパイル済み言語などはありません(Cはほとんど解釈されず、LISPは以前はほとんどコンパイルされていませんでしたが、これは実際には重要ではありません)。2.よく知られているほとんどの言語には、解釈、コンパイル、および混合の実装があり、コンパイルまたは解釈を妨げる言語はありません。
maaartinus

6
あなたの議論はとても理にかなっていると思います。grokの重要なポイントは、どのように見たいとしても、常に「ネイティブプログラム」または「ネバー」を実行することです。Windows上のexeはそれ自体実行可能ファイルではありません。起動するためにローダーと他のOS機能が必要で、実際には部分的に「解釈」されます。これは、.net実行可能ファイルを使用する場合に明らかになります。java myprogできるだけ多くの又は少ない天然であるgrep myname /etc/passwdか、ld.so myprogそれは引数をとり、データと動作を行う(すなわち、あらゆる手段)実行される場合があります。
ピーターA.シュナイダー

3

詳細は実装によって異なりますが、一般的には次のいくつかの組み合わせがあります。

  • GCを含むランタイムライブラリ。これはメモリ割り当てを処理し、「GC_now」関数を含む他のいくつかのエントリポイントを持ちます。
  • コンパイラは、GCのテーブルを作成して、どのデータ型のどのフィールドが参照されているかを認識します。これは、GCがスタックからトレースできるように、各関数のスタックフレームに対しても実行されます。
  • GCがインクリメンタル(GCアクティビティがプログラムでインターリーブ)またはコンカレント(個別のスレッドで実行)の場合、コンパイラは参照が更新されたときにGCデータ構造を更新する特別なオブジェクトコードも含みます。この2つには、データの一貫性に関して同様の問題があります。

インクリメンタルGCとコンカレントGCでは、コンパイルされたコードとGCが協力して、不変条件を維持する必要があります。たとえば、コピーコレクターでは、GCはスペースAからスペースBにライブデータをコピーし、ゴミを残して動作します。次のサイクルでは、AとBを反転させて繰り返します。したがって、1つのルールは、ユーザープログラムがスペースAのオブジェクトを参照しようとするたびに、これが検出され、オブジェクトがすぐにスペースBにコピーされ、プログラムが引き続きアクセスできるようにすることです。転送アドレスがスペースAに残されて、GCにこれが発生したことを示し、オブジェクトへの他の参照がトレースされるときに更新されるようにします。これは「読み取りバリア」として知られています。

GCアルゴリズムは60年代から研究されており、このテーマに関する広範な文献があります。さらに情報が必要な場合はGoogle。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.