Boehm GC を使う

2005.2.27新規作成。(2001.7.14, 2001.7.15 の日記に加筆。)

(2020.1) 追記。

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()で行う。

C++
[RAW]
  1. #include <unistd.h>
  2. #include <gc.h> // GC_MALLOC()
  3. //#include <stdlib.h> // malloc()
  4. #define ARRAY_SIZE 10000
  5. // malloc() の場合は, OOM Killer に kill される.
  6. void c_test() {
  7. char* ary[ARRAY_SIZE];
  8. while (1) {
  9. int i;
  10. for (i = 0; i < ARRAY_SIZE; i++)
  11. ary[i] = (char*) /*malloc(100000);*/ GC_MALLOC(100000);
  12. usleep(1);
  13. (void)ary[0];
  14. }
  15. }
  16. int main() {
  17. c_test();
  18. return 0;
  19. }

コンパイル・リンクは、次のようにする。-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を書かなくてもいい。

次のサンプルは大きなメモリを確保するオブジェクトを大量に生成する。

C++
[RAW]
  1. #include <stdio.h>
  2. #include <unistd.h>
  3. #include <gc_cpp.h> // C++ version
  4. class Hoge: public gc {
  5. char* ptr;
  6. public:
  7. Hoge() { alloc(); }
  8. void alloc() {
  9. // gc派生型ではないので, placement を使う
  10. ptr = new (GC) char[100000];
  11. }
  12. virtual ~Hoge() {
  13. printf("destructor called.\n"); // あえて解放しない
  14. }
  15. };
  16. int main() {
  17. Hoge* p[10000];
  18. while (true) {
  19. for (int i = 0; i < 10000; i++) {
  20. p[i] = new Hoge; // gc派生型なので、単に new でよい.
  21. p[i]->alloc(); // 二重確保
  22. usleep(1);
  23. (void) p[i];
  24. }
  25. }
  26. return 0;
  27. }

上の例では, ガベージコレクタによってオブジェクトが回収されるときに, デストラクタが自動的に呼び出されない。もちろん delete演算子を使えばデストラクタが呼び出されるが、解放をガベージコレクタに任せたいというモチベイションと相容れない。

メモリ以外のリソース解放については、デストラクタに頼らずに、明示的に行うようにしなければならない。

回収されるときにデストラクタが呼び出されるようにするには

(2020.1) ソースコードを更新。

ファイルなどメモリ以外の資源を解放しなければならない場合, タイミングが不定でもいいからデストラクタを自動的に呼び出してほしい。

クラスをgc_cleanupクラスから派生すればよい. そのクラスのオブジェクトがガベージコレクトされるときに自動的にデストラクタが呼び出される。また、定期的にガベージコレクトさせるには、イベントループなどでアイドル状態のときに GC_gcollect() を呼び出すようにするといいだろう。

配列については、一層難しい。

new演算子の書き方によって、どのnewメソッドが呼び出されるかは、次のように決まっている。生成する要素数はnewメソッドには渡らないことに注意。第一引数は, (vtableを含めた) 確保すべきメモリの大きさになっている。

new演算子の書き方呼び出されるnewメソッド
new T void* operator new(sizeof(T))
new (2, f) T void* operator new(sizeof(T), 2, f)
new T[5] void* operator new[](sizeof(T) * 5 + magic)
new (2, f) T[5] void* operator new[](sizeof(T) * 5 + magic, 2, f)

これを踏まえて、次のサンプル;

C++
[RAW]
  1. #include <stdio.h>
  2. #include <unistd.h>
  3. #include <gc/gc_cpp.h>
  4. #include <gc/javaxfc.h>
  5. static int global_cnt = 0;
  6. // Linux, x64
  7. constexpr int MAGIC_PADDING = 8;
  8. // gc_cleanup から派生させると, ガベージコレクトされるときにデストラクタか呼び
  9. // 出される。
  10. class Hoge: public gc_cleanup
  11. {
  12. char* ptr;
  13. int cnt;
  14. public:
  15. Hoge(): cnt(++global_cnt) {
  16. printf("hoge ctor: %d\n", cnt);
  17. ptr = new (GC) char[100000];
  18. }
  19. virtual ~Hoge() { printf(" dtor: %d\n", cnt); }
  20. };
  21. // @param memory 解放するメモリブロック。ABI (OS + CPU) に基づき, 先頭にpadding
  22. // がある.
  23. static void GC_CALLBACK MyHogeCleanUp(void* memory, void* array_size )
  24. {
  25. printf("MyHogeCleanUp() called: %p, %ld.\n", memory, (intptr_t) array_size );
  26. if (array_size) {
  27. // 注意! この方法はポータブルではない.
  28. Hoge* ptr = (Hoge*) ((char*) memory + MAGIC_PADDING);
  29. for (int i = 0; i < (intptr_t) array_size; i++)
  30. ptr[i].~Hoge();
  31. }
  32. }
  33. void cpp_test() {
  34. Hoge* foo = new Hoge();
  35. (void) foo;
  36. /*
  37. Hoge* ary;
  38. // この書き方では, デストラクタは, 100回ではなく, 先頭に対して1回しか呼び出さ
  39. // れない.
  40. ary = new Hoge[100];
  41. */
  42. // 次のようにすれば, 配列のガベージコレクト時に, コールバック関数を呼び出させ
  43. // ることができる。
  44. Hoge* ary = ::new (UseGC, MyHogeCleanUp, (void*) 100) Hoge[100];
  45. (void) ary;
  46. }
  47. int main() {
  48. // Should call GC_INIT().
  49. GC_INIT();
  50. cpp_test();
  51. usleep(0);
  52. GC_gcollect();
  53. GC_finalize_all(); // これがないと, すべてのデストラクタが呼び出されるとは限
  54. // らない.
  55. return 0;
  56. }

単純に new Hoge[100] とすると、ガベージコレクト時に、2番目以降のそれぞれの要素についてデストラクタが呼び出されない。C++ の作り上, new[] メソッドには必要メモリしか渡されないため、いかんともしがたい。

上の例では, placement 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| }

最初に戻って、ガベージコレクタを使うからには、デスクトラクタに依存しないように組み立てるほうが良さそうだ。

リンク

サイト内関連文書:

外部: