メモリ管理
(1)
- C++のスマートポインタや、Rustの所有権の考え方など、メモリ管理をできるだけ自動化しようという風潮があるように思います。
- プログラマにとって一番気楽なのはガーベージコレクションのある言語を使うことでしょう。オブジェクトを使わなくなれば自動で回収してくれるからです。しかしこれを実現するためにはポインタ変数のスキャンが必要になるので、処理の負荷としてはかなり大きなものになります。
- C++のスマートポインタやRustの所有権の考え方は、自動変数の寿命がスコープで消えることを利用して、そのタイミングで自動でメモリ解放をしようというものです。とてもよく考えられていると思います。
- 私はC言語でスマートポインタみたいなものを実現するにはどうしたらいいかを考えて、似たような仕組みを作ったのですが、出来上がってみると、これはまた別の応用が可能だなと気づいたのでそれを紹介します。
(2)
- まずC言語では自動変数の寿命が切れても、そのタイミングでfreeやオブジェクトのデストラクタを呼びだすような仕組みはありません。
- そこで、Cleanというクラス(の代わりの構造体と関数群)を作って、mallocするたびにCleanオブジェクトにfreeを呼びだすための情報を自動で登録していき、init関数は自動でdeinit関数を登録していき、スコープの末尾でプログラマが明示的にClean_outを呼び出して、登録された関数を呼び出してメモリ解放を実現します。
- このClean_outをやり忘れるようなことがあると解放忘れになってメモリリークということになりますが、個々のオブジェクトの管理をしなければいけないもとの状況よりは、Cleanオブジェクトだけ考えていればいい状態になったので、だいぶ楽です。
[例1]
MyObjA *a = myMalloc(sizeof (MyObjA), &clean); // こう書けば、もう free(a); をしなくていい.
MyObjA_init(a, &clean); // こう書けば、もう MyObjA_deinit(a); をしなくていい.
// まあしかし、私ならinitの処理内容を工夫して、NULLを渡したときには自動でmyMallocするように作るので、上記の2行は以下の1行で書ける.
MyObjA *a = MyObjA_init(0, &clean);
(3)
- Cleanオブジェクトは入れ子にできます。これはどういうことかというと、新規にCleanオブジェクトをinitするときに、すでにあるCleanオブジェクトにdeinitを登録しておけるということです。
- そうすると、もし子のCleanオブジェクトがClean_outし忘れても、親がClean_outしたときに、「あれ?このCleanオブジェクトは空になってないからClean_outを忘れていたようだよ!」というエラーを出せます。これで上記の「解放忘れ問題」はほぼなくせるでしょう。
- また Clean_out のし忘れを見つけても、エラーにしないで、代わりに黙ってClean_outしてくれる、Clean_forceというのもあります。これが何の役に立つのかというと、longjmpで飛んできたときに、使い終わったすべてのオブジェクトを一気に片づけられるということです。
(4)
- 例えば、与えられた数値を16進数文字列にして返す関数があったとします。
const char *toHex(int i)
{
char s[100];
sprintf(s, "%x", i);
char *p = malloc(strlen(s) + 1);
strcpy(p, s);
return p;
}
- これは受け取った文字列を使い終わった後にfreeしなければいけません。
- それが面倒なので、Cleanオブジェクトに自動登録するようにします。
const char *toHex(int i, Clean *c)
{
char s[100];
sprintf(s, "%x", i);
char *p = myMalloc(strlen(s) + 1, c);
strcpy(p, s);
return p;
}
- (ここまでは別に普通なのですが、ここからが面白いところです。)
- ここで、この関数はかなりの頻度でi=0で呼び出されており、だからそれを高速化できたら、とてもうれしいとします。ということでこうします。
const char *toHex(int i, Clean *c)
{
if (i == 0) return "0"; // この行追加.
char s[100];
sprintf(s, "%x", i);
char *p = myMalloc(strlen(s) + 1, c);
strcpy(p, s);
return p;
}
- つまりこの関数は、ヒープエリア内のポインタを返すか、それとも文字リテラルへのポインタを返すか、その二通りがあるということです。
- もしCleanオブジェクトがなければ、その違いは使用後にfreeする必要があるかないかという重大な差になるので、こういう高速化はたぶんやりません(その判定がめんどくさい)。しかしCleanオブジェクトが使えると、freeする責任が呼び出し元ではなくなるので、こういう高速化が簡単にできるようになったわけです。
- 一般に、mallocやfreeは結構重たい関数なので、もしも前提条件のような状況下なら、この最適化は結構効き目があるはずです。
(5)
- なお、このCleanクラスは、Objective-CのAutoReleaseの仕組みが面白いと思って真似しているうちに思い付きました。