KH-FDPLのノート0002

  • (by K, 2015.03.17)

(1) 一般的なオブジェクトの寿命の管理モデル

  • C++ではnewでオブジェクトを作ってdeleteで破棄していました。また自動変数(スタック上にとられる変数)はスコープから抜けるときに自動でデストラクタが呼び出されていました。
    • この自動変数の挙動は便利で分かりやすくて深刻なメモリリークバグも起こりにくいと思いますが、自分でnewして作ったオブジェクトについてはdeleteを呼び忘れたりしてメモリリークバグの大きな原因となっています。
  • JavaではGC(ガーベージコレクション)を採用し、プログラマがdeleteしないでもいいようにしました。
    • これは一見すると非常にかっこいいのですが、実際は処理系が未使用のオブジェクトをうまく検出できずにメモリリークしてしまったり(特に循環参照などが起こるとリークしやすい)、full-GCが始まるとプログラムがしばらくの間とまってしまう問題があります。
    • GCは多くの場合ではプログラマを寿命管理から解放してくれます。しかしそのせいでこれに配慮しないことに慣れてしまい、リークやfull-GCに悩まされ始めた時にどうしていいかわからなくて途方に暮れることになります。これはあまり教育的な仕様とは言えないでしょう。
  • これらに対する比較的新しい方法として、autorelease方式があります。iOSのObjective-Cで使われています。
    • これはとても良い方法だと思いました。KH-FDPLでも最初はこの方法をそのまま取り入れようと思っていたくらいです。
    • ただautoreleaseしない場合に、retainやreleaseで参照カウンタを管理しなければいけません。その部分の難易度が従来と大差ないと思いました。ここで間違えればやはりメモリリークしてしまいます。
    • 最近のarcの仕組みを使うと、retainやreleaseを意識しなくても自動でかなりうまくやってくれるそうです。もはや循環参照以外でリークすることはほとんどないそうです。

(2) KH-FDPLのオブジェクトの寿命の管理モデル

  • KH-FDPLは参照カウンタを持ちません。結局こういうものをプログラマに管理させていると、永久にメモリリーク問題はなくならないと思うからです。
  • 短期間で作って壊すようなオブジェクトは、まさにautoreleaseと同じ方法で管理します。次回の回収時に回収されるプールがあるので、そこに登録するわけです。
  • そしてそれより長く生き残るオブジェクトに関しては、どのプールに所属するかを「new時に」指定します。どんなプログラムも、最低一つのプールを持っていますし、関数呼び出しをすればそのたびにプールが作られます。それらの中のどのプールに所属するのかをnew時に指定すればいいのです。
    • これにより、そのプールが回収されるときに必ずオブジェクトは回収されます。リークしません。
    • もし寿命を変更したくなれば、あとから所属するプールを変更すればいいです。それで問題ありません。
  • プールを指定するなんて言うとややこしく聞こえますが、ファイルを作るときに所属パスを指定するのと同じくらいの感覚です。何も指定しなければ、次回回収プールがデフォルトで選ばれます(autorelease相当)。関数の戻り値用のオブジェクトは、次々回回収プールがデフォルトになります。
  • そもそもオブジェクトを作るときが、もっともオブジェクトの寿命について考えているときでもあるのです。オブジェクトを作るときは、そのオブジェクトを何のために作るのかわかっていますし(目的)、だからいついらなくなるのかもわかっているのです。
  • KH-FDPLはオブジェクトが基本的に永続性なので、メモリリークは非常に深刻です。 だからこんなにリークさせない仕組みにこだわっているのです。
  • KH-FDPLでリークを起こしてしまったら、それは再起動後にも残るので、放置していると永久に残ります。他の言語環境ならリークしても再起動すれば済みますが、KH-FDPLではそうは行かないわけです。まあ自分でせっせとリークしたオブジェクトを特定して消していけばいいといえばそれまでですが、それよりはリークを起こさない仕組みを考えるほうが建設的だと思います。

(3) じゃあ他の管理アルゴリズムは使えないのか?

  • KH-FDPLは多様な言語を受け入れたいと思っています。GCが好きな人はGCのあるプログラミング言語を書きたいでしょうし、new-deleteモデルが好きな人はそういうプログラミング言語を書きたいでしょう。・・・OKです。
  • まずは簡単なnew-deleteモデルから行きましょう。このモデルの言語では、オブジェクトをすべて一つのプールに所属させておいて、そのプールはアプリが終了するまで残るようにしておきます。これで勝手に消えることは一切なくなります。それで、delete命令が来たら、そのプールから個別に削除してやればいいだけです。そういう動作をするバイトコードをコンパイラが生成すればいいだけです。
  • GCの場合も同様です。GCスレッドは、プールの中のすべてのオブジェクトをスキャンすることができます。それで誰からも参照されていないオブジェクトを見つけたら、それを個別に削除すればいいだけです。簡単ですね!

(4) KH-FDPLの方法で本当にうまくいくのか? [ここはかなり具体的なのでより詳しく知りたい人向け]

  • new-deleteモデルは、「使わなくなったらdeleteしてください」というだけなので、管理さえできればうまくいくのは自明です。またGCモデルも「誰からも参照されなくなったら勝手に消えます」というだけなので、やはりうまくいくのは自明です。しかし、KH-FDPLは、本当にそれでうまくいくの?と不安になるかもしれません。ということで、詳しい解説を試みます。
  • KH-FDPLでは、関数を呼ぶ直前に「スタックフレーム」という「データストア型オブジェクト」を作ります。とりあえずイメージとしてはフォルダみたいなものを考えてください。これがプールになります。
  • 関数はローカル変数を使います。そうするとそれらのローカル変数はそのスタックフレームの中に作られていきます。こうすることで他の関数の変数と名前が衝突する心配がありません。
  • 関数から抜けると、スタックフレームは削除されます(もしかしたらすぐには消さずに、次の関数呼び出しの直前に消されるかもしれない)。つまりローカル変数は消えることになります。
  • 関数が関数を呼び出した場合、スタックフレームの中にスタックフレームが作られて、そこが使われます。こういう仕組みなので再帰とかでも大丈夫です。
  • ローカル変数についてはこれで良いとして、それではそれ以外のオブジェクトはどうでしょうか。何か複雑なオブジェクトを作ってそれを返す関数、なんていうのはありふれたことです。
  • この場合、そのオブジェクトを「どこに作るのか」を呼び出し元が指定します。つまり何層にも重なったスタックフレームのうちのどれにするか、ということです。深いところのスタックフレームならそれだけ長い間消えずに残ることになります。そうやって長生きするオブジェクトを作った後なら、そのオブジェクトに対する参照がどれだけあちこちで作られても問題なしです。循環参照とかがあっても別にいいです。とにかく関数から抜けて、更に関数から抜けて、そうやって何段かの階層を抜ければ、そのタイミングでこのオブジェクトも消えます。「mainから抜けるまではずっとそのオブジェクトにアクセスしうる」という状況なら、mainのスタックフレームにオブジェクト作れらせればいいのです。それだけのことなのです。
  • しかし、そもそもsubという関数が呼び出し元であるmainのスタックフレームを指定できるものなのでしょうか。つまり子は親のスタックフレームにアクセスできていいのでしょうか?これは簡単に言えば、フォルダをさかのぼる「..」があってもよいのかということです。こんなものがあれば、どんどんさかのぼってやがてはルートにだどりつけてしまいます。これはセキュリティ上問題があります。・・・ということで、subは勝手にはmainのスタックフレームを指定できません。subはmainからスタックフレームを引数でもらっているのです。
  • ここまでをまとめます。
    main()
    {
        sub0(@stack);
        ...
    }
    
    sub0(pool)
    {
        sub1(pool);
        ...
    }
    
    sub1(pool)
    {
        pool内にオブジェクトを作る命令;
        ...
    }
  • こんな感じになります。sub1が実際にオブジェクトを作っているところですが、その際にmainのスタックフレームを使っています。それを実現するために、main内の関数呼び出しでは引数にスタックフレームを指定していますし、sub0ではそれをリレーしています。
  • でももっとカッコよくやるならこうだと思います。
    main()
    {
        work = sub0();
        ...
    }
    
    sub0()
    {
        returnValue = sub1();
        ...
        return returnValue;
    }
    
    sub1()
    {
        obj.foo = ...;
        obj.bar = ...;
        ...
        return obj;
    }
  • 結果的に、sub1が作ったオブジェクトはmainのworkとして残ることになります。
  • より詳細な流れ:
    • (a) システムはmainを呼び出すにあたって@stackというフォルダを作り、そこをカレントにする。
    • (b) main()はさらに@stackというフォルダを作ってそこをカレントにした後、sub0()を呼び出す。
    • (c) sub0()はさらに@stackというフォルダを作ってそこをカレントにした後、sub1()を呼び出す。
    • (d) sub1()はカレントディレクトリにobjというフォルダを作って、その中にfooやbarを作っていく。
    • (e) sub1()でのreturn命令により、objは@retValにリネームされる。そしてsub1()を抜けてsub0()に戻る。
    • (f) sub0()ではフォルダの階層を元に戻す。その上で@stack.@retValというオブジェクトをreturnValueにリネームする。
      • どうせ@stackは次の関数呼び出しまでには消える運命なので、@retValをreturnValueにコピーする必要はなく、リネームで十分。リネームならオブジェクトのIDも変わらない。
    • (g) sub0()でのreturn命令により、returnValueは@retValにリネームされる。そしてsub0()を抜けてmain()に戻る。
    • (h) main()ではフォルダの階層を元に戻す。その上で@stack.@retValというオブジェクトをworkにリネームする。

こめんと欄


コメントお名前NameLink

トップ   編集 凍結解除 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2016-05-10 (火) 18:54:05 (3131d)