メモリの割り当てをいつ解除するかを静的に予測することは可能ですか?ソースコードからのみですか?


27

メモリ(およびリソースロック)は、プログラムの実行中の確定的なポイントでOSに返されます。プログラムの制御フローだけで、特定のリソースの割り当てを確実に解除できる場所を知るのに十分です。人間のプログラマーがfclose(file)、プログラムが完了したときにどこに書くべきかを知っているように。

GCは、制御フローが実行されるランタイム中に直接把握することでこれを解決します。しかし、制御フローについての真実の本当のソースはソースです。したがって、理論的にはfree()、ソース(またはAST)を分析することで、コンパイルの前に呼び出しを挿入する場所を決定できるはずです。

参照カウントはこれを実装するための明らかな方法ですが、ポインターがまだ参照されている(まだスコープ内にある)けれども不要になった状況に遭遇するのは簡単です。これは、ポインタを手動で割り当て解除する責任を、それらのポインタへのスコープ/参照を手動で管理する責任に変換するだけです。

プログラムのソースを読み取ることができるプログラムを作成することが可能であるように思われます。

  1. プログラムの制御フローのすべての順列を予測する---プログラムのライブ実行を監視するのと同様の精度
  2. 割り当てられたリソースへのすべての参照を追跡する
  3. 参照ごとに、参照が絶対に参照解除されないことが保証される最も早いポイントを見つけるために、後続の制御フロー全体をトラバースします。
  4. その時点で、ソースコードのその行に割り当て解除ステートメントを挿入します

すでにこれを行うものはありますか?RustやC ++スマートポインター/ RAIIは同じものだとは思わない。


57
停止する問題を調べてください。「プログラムがXを実行するかどうかをコンパイラーが判断できないのか」という質問の祖父です。常に「一般的なケースではない」と回答されます。
ラチェットフリーク

18
メモリ(およびリソースロック)は、プログラムの実行中の確定的なポイントでOSに返されます。いいえ
陶酔

9
@ratchetfreakおかげで、化学の代わりにcomp sciで学位を取得したいのに、この停止するような問題を知ることはありません。
ゼルコン

15
@ zelcon5、あなたは今、化学停止の問題について知っています... :)
デビッドアルノ

7
@Euphoricリソースを使用する場合の境界はRAIIを持つかトライして、リソースのような非常にはっきりしているので、あなたのプログラムを構築しない限り、
ラチェットフリーク

回答:


23

この(考えられた)例を見てください:

void* resource1;
void* resource2;

while(true){

    int input = getInputFromUser();

    switch(input){
        case 1: resource1 = malloc(500); break;
        case 2: resource2 = resource1; break;
        case 3: useResource(resource1); useResource(resource2); break;
    }
}

freeはいつ呼び出されるべきですか?mallocとassignの前にresource1、それはにコピーされる可能性があるためresource2resource2できません

確認する唯一の方法は、resource1とresource2をテストして、ケース1と2で等しくないかどうかを確認し、等しくない場合は古い値を解放することです。これは基本的に参照カウントであり、可能な参照は2つしかないことがわかっています。


実際、それが唯一の方法ではありません。もう1つの方法は 1つのコピーのみを許可することです。もちろん、これには独自の問題があります。
ジャックエイドリー

27

RAIIは自動的に同じものではありませんが、同じ効果があります。「これにアクセスできなくなったとき、どうやって知るのですか?」という質問に対する簡単な答えを提供します。使用してスコープを特定のリソースが使用されているときにエリアをカバーします。

「プログラムが実行時に型エラーを起こさないことをどのようにして知ることができますか?」という同様の問題を検討したいかもしれません。これに対する解決策は、プログラムを通るすべての実行パスを予測するのではなく、型の注釈と推論のシステムを使用して、そのようなエラーが発生しないことを証明することです。Rustは、この証明プロパティをメモリ割り当てに拡張する試みです。

停止の問題を解決することなく、プログラムの動作に関する証明を書くことができますが、それは、何らかの種類の注釈を使用してプログラムを制約する場合のみです。セキュリティプルーフ(sel4など)も参照してください。


コメントは詳細なディスカッション用ではありません。この会話はチャットに移動さました
maple_shaft

13

はい、これは野生に存在します。MLキットは、使用可能なメモリ管理オプションの1つとして説明されている戦略(多かれ少なかれ)を備えた製品品質のコンパイラです。また、従来のGCの使用または参照カウントとのハイブリッド化が可能になります(ヒーププロファイラーを使用して、どの戦略が実際にプログラムに最適な結果をもたらすかを確認できます)。

領域ベースのメモリ管理に関する回顧展は、MLキットの元の著者による記事であり、成功と失敗について説明しています。最終的な結論は、ヒーププロファイラーの助けを借りて書くとき、戦略が実用的であるということです。

(これは、実際のエンジニアリングの質問に対する答えを得るために、通常、停止問題に目を向けるべきではない理由の良い例です:最も現実的なプログラムの一般的なケースを解決したくない、または必要としませ。)


5
これは停止問題の適切な適用の優れた例だと思います。停止する問題は、一般的なケースでは問題を解決できないことを示しているため、問題を解決できる限られたシナリオを探します。
-Taemyr

Standard MLやHaskellのような純粋な、またはほぼ純粋な機能的で副作用のない言語について話すとき、問題ははるか解決可能になることに注意してください
cat

10

プログラムの制御フローのすべての順列を予測する

これが問題のある場所です。順列の量は任意の非自明なプログラムに対して非常に膨大であり(実際には無限です)、必要な時間とメモリがこれを完全に非実用的にします。


いい視点ね。量子プロセッサがあれば、それが唯一の希望だと思います
-zelcon

4
@ zelcon5母、いや。量子コンピューティングはこれを悪化させるが、改善はしない。プログラムに追加の(「隠された」)変数を追加し、さらに不確実性を高めます。私が見た最も実用的なQCコードは、「高速計算のための量子、確認のための古典」に依存しています。私は量子コンピューティングの表面をかろうじて傷つけただけですが、量子コンピューターは、古典的なコンピューターがバックアップして結果を確認しない限り、あまり有用ではないように思えます。
ルアーン

8

停止する問題は、これがすべての場合に可能ではないことを証明しています。ただし、非常に多くの場合、それは依然として可能です。実際、ほとんどすべてのコンパイラーによって、おそらく大部分の変数に対して行われます。これは、長期のヒープストレージではなく、スタックまたはレジスタに変数を割り当てるだけで安全だとコンパイラが判断できる方法です。

純粋な関数または非常に優れた所有セマンティクスがある場合は、その静的分析をさらに拡張できますが、コードの分岐数が増えるほど、コストが非常に高くなります。


コンパイラ、メモリを解放できると考えています。しかし、そうではないかもしれません。一般的な初心者のエラーを考えて、ローカル変数へのポインターまたは参照を返します。些細なケースはコンパイラーによってキャッチされます。それほどささいなものはそうではありません。
ピーター-モニカーの復活

その間違いは、プログラマが手動でメモリ割り当て@Peterを管理しなければならない言語のプログラマによって行われますコンパイラがメモリ割り当てを管理するとき、これらの種類の間違いは起こりません。
カールビーレフェルト

さて、あなたは「ほとんどすべてのコンパイラ」というフレーズを含む非常に一般的なステートメントを作成しました。これにはCコンパイラを含める必要があります。
ピーター-モニカの復活

2
Cコンパイラはこれを使用して、レジスタに割り当てることができる一時変数を決定します。
カールビーレフェルト

4

1人のプログラマまたはチームがプログラム全体を作成する場合、メモリ(およびその他のリソース)を解放する必要がある場所を設計ポイントとして識別することができます。したがって、はい、より限定的なコンテキストでは、設計の静的分析で十分な場合があります。

ただし、サードパーティのDLL、API、フレームワークを考慮する(およびスレッドをスローする)場合、使用しているプログラマーがどのエンティティがどのメモリを所有しているかを正しく推論することは非常に困難です(すべての場合において不可能)最後に使用したとき。私たちの通常の言語の容疑者は、オブジェクトと配列のメモリ所有権の移転を十分に文書化していません。プログラマーが(静的または動的に!)を判断できない場合、コンパイラーもおそらくできないでしょう。繰り返しますが、これはメモリ所有権の転送がメソッド呼び出しやインターフェイスなどでキャプチャされないためです。したがって、メモリ内のいつ、どこでコードを解放するかを静的に予測することはできません。

これは非常に深刻な問題であるため、最新の言語の多くはガベージコレクションを選択します。これにより、最後のライブ参照の後にメモリが自動的に解放されます。ただし、GCにはかなりのパフォーマンスコストがかかりますが(特にリアルタイムアプリケーションの場合)、万能薬ではありません。さらに、GCを使用してメモリリークが発生する可能性があります(たとえば、増加するだけのコレクション)。それでも、これはほとんどのプログラミング演習に適したソリューションです。

いくつかの選択肢があります(いくつかは新たに登場しています)。

Rust言語はRAIIを極端なものにします。これは、クラスとインターフェースのメソッドで所有権の移転をより詳細に定義する言語構造を提供します。たとえば、呼び出し元と呼び出し先の間で転送されるオブジェクトと借用されるオブジェクト、またはより長い寿命のオブジェクトです。これにより、メモリ管理に対するコンパイル時の安全性が高くなります。ただし、取り上げるのは簡単な言語ではなく、問題がないわけでもありません(たとえば、デザインが完全に安定しているとは思わない、特定のことはまだ実験されているため、変更されています)。

SwiftとObjective-Cは、さらに別のルートに進みます。これは、ほとんど自動化された参照カウントです。参照カウントはサイクルの問題になります。たとえば、特にクロージャーでは、プログラマーにとって大きな課題があります。


3
もちろん、GCにはコストがかかりますが、パフォーマンス上のメリットもあります。たとえば、.NETでは、ヒープからの割り当ては「スタック割り当て」パターンを使用するため、ほとんど無料です。ポインタをインクリメントするだけです。.NET GCを中心に、手動でメモリ割り当てを使用するよりも高速に書き換えられるアプリケーションを見てきましたが、実際には明確ではありません。同様に、参照カウントは実際には非常に高価(GCとは異なる場所で)であり、回避できる場合は支払いたくないものです。リアルタイムのパフォーマンスが必要な場合、多くの場合、静的割り当てが唯一の方法です。
ルアーン

2

プログラムが未知の入力に依存しない場合は、はい、可能です(複雑なタスクで時間がかかる可能性がありますが、それはプログラムにも当てはまります)。このようなプログラムは、コンパイル時に完全に解決可能です。C ++の用語では、(ほぼ)完全にconstexprsで構成できます。簡単な例は、piの最初の100桁を計算するか、既知の辞書をソートすることです。


2

一般に、メモリの解放は停止の問題と同等です。プログラムが(静的に)停止するかどうかを静的に判断できない場合、メモリを(静的に)解放するかどうかも判断できません。

function foo(int a) {
    void *p = malloc(1);
    ... do something which may, or may not, halt ...
    free(p);
}

https://en.wikipedia.org/wiki/Halting_problem

とはいえ、Rustは非常に素晴らしい... https://doc.rust-lang.org/book/ownership.html

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