Boehm GC を使う
2005.2.27新規作成。(2001.7.14, 2001.7.15 の日記に加筆。)
C/C++には、言語仕様としてのガベージコレクションの機能はない。動的に確保したメモリは、自分で、ただ一度だけ解放するようにプログラムを組まなければならない。解放を忘れるとメモリリークが発生し、解放しすぎるとプログラムがクラッシュすることもある。
ガベージコレクタを使うと、メモリを好きなだけ確保するだけでよく、解放は自動的にしてくれる。巨大なメモリを明示的に解放するような場合のほかは、メモリを解放するコードを書く必要はない。
The Boehm-Demers-Weiser conservative garbage collector (Boehm GC) は、C言語用のメジャーなガベージコレクタ。さまざまなソフトウェアがこのライブラリを使っている。しかし、Fedora Core 3には含まれない。
Boehm GCは、保守的GCという方法でガベージコレクトする。定期的にメモリ内を走査し、使われていないと見られるオブジェクトのメモリを回収していく。Boehm GC以外では、boost::shared_ptrが参照カウント方式を用いている。
このページでは、インストール方法のほか、簡単な使い方についても書いてみる。
インストール
入手先;A garbage collector for C and C++
2005年2月現在の最新版は、バージョン6.4。
アーカイブを展開したら、configureして、makeするだけ。configureには、次の引数を与えてみた。これでC++用ライブラリがインストールされる。
$ ./configure --enable-cplusplus $ make $ su # make install
C言語で使う
既存のプログラムでガベージコレクタを使うようにするには、メモリの確保(や必要があれば解放)の部分を修正しなければならない。
まずは単純なサンプルから。次のファイルは、とにかくメモリをどんどん確保していく。GCを使う場合は、ヘッダー<gc.h>をincludeした上で、メモリの確保をGC_MALLOC()で行う。
2| #include <unistd.h> 3| #include <gc.h> 4| 5| void c_test() { 6| char* ary[100]; 7| while (1) { 8| int i; 9| for (i = 0; i < 100; i++) 10| ary[i] = (char*) GC_MALLOC(10000); 11| usleep(1); 12| } 13| } 14| 15| int main() { 16| c_test(); 17| return 0; 18| }
コンパイル・リンクは、次のようにする。-lオプションで、libgc.soをリンクする。
$ gcc -Wall -I/usr/local/include first.c -lgc
実行中にtopコマンドなどで見ると、使用メモリが増えていかないことが分かる。GCを使わずにmalloc()でメモリを確保するようにすると、当たり前だが、どんどん使用メモリが増えていく。
C++で使う
ガベージコレクトさせるには、メモリの確保をBoehm GCにさせる必要がある。やり方は二種類ある。一つは、オブジェクトをnew演算子で生成するときにplacementを使う。new (GC) 型名 と書く。もう一つは、クラスをclass gcから派生させる。このときは、そのクラスのオブジェクトを生成するときにplacementを書かなくてもいい。
次のサンプルは大きなメモリを確保するオブジェクトを大量に生成する。
2| #include <stdio.h> 3| #include <unistd.h> 4| #include <gc_cpp.h> 5| 6| class Hoge: public gc { 7| char* ptr; 8| public: 9| Hoge() { 10| ptr = new (GC) char[1000000]; 11| } 12| 13| virtual ~Hoge() { 14| printf("destructor called.\n"); 15| } 16| }; 17| 18| int main() { 19| Hoge* p; 20| while (true) { 21| p = new Hoge; 22| usleep(1); 23| } 24| return 0; 25| }
ガベージコレクタを使う場合、オブジェクトが回収されるときにデストラクタが呼び出されない。もちろんdelete演算子を使えばデストラクタが呼び出されるが、それではGCを使う意味がない。既存のプログラムをGCを使うように修正する場合には、デストラクタに書いているコードを修正しなければならない。
回収されるときにデストラクタが呼び出されるようにするには
とは言うものの、ファイルなどメモリ以外の資源を解放するなど、タイミングが不定でもいいからデストラクタを自動的に呼び出してほしいときがある。
クラスをgc_cleanupクラスから派生すると,そのクラスのオブジェクトがガベージコレクトされるときに自動的にデストラクタが呼び出される。また、定期的にガベージコレクトさせるには、イベントループなどでアイドル状態のときにGC_gcollect()を呼び出すようにするといいだろう。
5| #include <stdio.h> 6| #include <unistd.h> 7| #include <gc_cpp.h> 8| 9| class Hoge: public gc_cleanup { 10| static int global_cnt; 11| char* ptr; 12| int cnt; 13| public: 14| Hoge(): cnt(++global_cnt) { 15| printf("hoge ctor: %d\n", cnt); 16| ptr = new (GC) char[1000000]; 17| } 18| virtual ~Hoge() { printf(" dtor: %d\n", cnt); } 19| }; 20| 21| int Hoge::global_cnt = 0; 22| 23| void cpp_test() { 24| Hoge* ary; 25| for (int i = 0; i < 10; i++) ary = new Hoge; 26| ary = NULL; // test 27| } 28| 29| int main() { 30| cpp_test(); 31| usleep(0); // これがないと10番目が解放されないことがある。 32| GC_gcollect(); 33| return 0; 34| }
上記のサンプルには、若干の問題がある。25行目を書き換えて、new Hoge[n]として配列を生成するようにすると、先頭のオブジェクトについてしかデストラクタが呼ばれない。
配列の各要素についてデストラクタを呼ぶには
new演算子の書き方によって、どのようなnewメソッドが呼び出されるかは、次のように決まっている。生成する要素数はnewメソッドには渡らないことに注意。第一引数は、(vtableを含めた)確保すべきメモリの大きさになっている。
| new演算子の書き方 | 呼び出されるnewメソッド |
|---|---|
| new T | operator new(sizeof(T)) |
| new (2, f) T | operator new(sizeof(T), 2, f) |
| new T[5] | operator new[](sizeof(T) * 5 + x) |
| new (2, f) T[5] | operator new[](sizeof(T) * 5 + y, 2, f) |
そのため、newメソッドの中では、各オブジェクトへのポインタを得ることはできない。配列を生成する場合は、次のように書くしかないように思う。配列の配列にしてしまって、一つずつオブジェクトを生成する。
26| void cpp_test() { 27| Hoge** ary; 28| for (int i = 0; i < 5; i++) { 29| ary = new (GC) Hoge*[2]; 30| ary[0] = new Hoge; 31| ary[1] = new Hoge; 32| } 33| ary = NULL; // test 34| }