K.Sasada's Home Page

Boehm GC を使ってみる


注意:後半部、速度の比較をやっていますが、どうやら相当いいかげんです。本気でこの比較を知りたければご自分でテストを作成し、ご確認ください。特に、最適化のあたりとか。一番いいのは、現在既にあるアプリを変更してやってみることかなぁ。

なにそれ?

Boehm GCを使おうを参照。

前提環境

準備

使ってみる。

doc/README.win32 を見ると、次のように書いてある。

In order to use the gc_cpp.h C++ interface, all client code should include gc_cpp.h.

うーん、面倒そうだ。ついでに

Clients may need to define GC_NOT_DLL before including gc.h, if the collector was built as a static library (as it normally is in the absence of thread support).

これで、スレッドはサポートしてくれない模様。まぁ、使えるでしょう。DLLにするの面倒だし、これでいいや。

用意したソースはこちら。

// t.cpp
// compile with : cl t.cpp -GX -IC:\app\prog\gc6.1alpha5\include /link gc.lib user32.lib
#define GC_NOT_DLL
#include "gc_cpp.h"
#include <iostream>

class hoe{
  char p[0x100];
};

main(){
  while(1){
    hoe *h = new hoe;
  }
  return 0;
}

user32.lib をリンクしてんのは、MessageBox を要求するからとりあえず。たしかに、Win32用だしなぁ。いつMessageBoxするんだろ。

結果。

・・・全然駄目じゃん。タスクマネージャー見てると、どんどんメモリ使用率が上がっていく。なんかまずい。きっとまずい。

というわけで、include/gc_cpp.h をよく読む。

うあ、全部 gc クラスを基底に持たなきゃいけないのか ^^; 。

というわけで、再挑戦。

// t.cpp
// compile with : cl t.cpp -GX -IC:\app\prog\gc6.1alpha5\include /link gc.lib user32.lib
#define GC_NOT_DLL
#include "gc_cpp.h"
#include <iostream>

class hoe : public gc{
  char p[0x100];
};

main(){
  while(1){
    hoe *h = new hoe;
  }
  return 0;
}

・・・。

おぉ、全然メモリ使用率増えませんよ。やったやった。

もうちょっとちゃんと使ってみる。

デストラクタは呼ばれるんだろうか? さっきのソースにデストラクタをつけてみる。

#define GC_NOT_DLL
#include "gc_cpp.h"
#include <iostream>

using namespace std;

class hoe :public gc{
  char p[0x100];
public:
  ~hoe(){
    cout << "collected!" << endl;
  }
};

main(){
  while(1){
    hoe *h = new hoe;
  }
  return 0;
}

結果。呼ばれない

そのためには gc じゃなく、 gc_cleanup を基底として持つらしい。

#define GC_NOT_DLL
#include "gc_cpp.h"
#include <iostream>

using namespace std;

class hoe :public gc_cleanup{
  char p[0x100];
public:
  ~hoe(){
    cout << "collected!" << endl;
  }
};

main(){
  while(1){
    hoe *h = new hoe;
  }
  return 0;
}

結果。たくさん collected されました

速度差

んー、こうなると、boost::shared_ptr とも比較したいのが人情。ということで、比較。

とにかくガンガンメモリを食っていく。GC起こりまくり。

とりあえず、GC 無し。

#include <iostream>
#include <boost/timer.hpp>
using namespace std;
using namespace boost;

class hoe{
  char p[0x100];
public:
  ~hoe(){
    cout << "collected!" << endl;
  }
  void nanika(){
    for(int i=0;i<0x100;i++){
      p[i] = i;
    }
  }
};


#define MAX_LOOP 1000000

main(){
  timer tmr;
  for(int i=0;i<MAX_LOOP;i++){
    hoe *h = new hoe;
    h->nanika();
  }
  cout << tmr.elapsed() << endl;
  return 0;
}

メモリを一杯食って、結果は 12.608 sec

次、share_ptr。(いや、auto_ptr で良かった・・・)。

// sp.cpp
// cl sp.cpp -GX -IC:\app\prog\boost_1_28_0
#include <iostream>
#include <boost/smart_ptr.hpp>
#include <boost/timer.hpp>
using namespace std;
using namespace boost;

class hoe{
  char p[0x100];
public:
  ~hoe(){
  }
  void nanika(){
    for(int i=0;i<0x100;i++){
      p[i] = i;
    }
  }
};


#define MAX_LOOP 1000000

main(){
  timer tmr;
  for(int i=0;i<MAX_LOOP;i++){
    shared_ptr<hoe>h(new hoe);
    h->nanika();
  }
  cout << tmr.elapsed() << endl;
  return 0;
}

結果。勿論メモリリークせず、 8.822 sec , 8.802 sec , 8.772 sec

次、BoehmGC 版。

// t.cpp
// cl t.cpp -GX -IC:\app\prog\gc6.1alpha5\include -IC:\app\prog\boost_1_28_0 /link gc.lib user32.lib
#define GC_NOT_DLL
#include "gc_cpp.h"
#include <iostream>
#include <boost/timer.hpp>

using namespace std;
using namespace boost;

class hoe :public gc_cleanup{
  char p[0x100];
public:
  ~hoe(){
    // cout << "collected!" << endl;
  }
  void nanika(){
    for(int i=0;i<0x100;i++){
      p[i] = i;
    }
  }
};

#define MAX_LOOP 1000000

main(){
  timer tmr;
  for(int i=0;i<MAX_LOOP;i++){
    hoe *h = new hoe;
    h->nanika();
  }
  cout << tmr.elapsed() << endl;
  return 0;
}

結果。16.984 sec , 17.324 sec , 16.804 sec

・・・駄目ジャン BoehmGC (T_T)。

いや、例が極端すぎるのだろうなぁ。それに、最適化のかかり具合とか、拠るだろうし。と言って弁護。そういえば、最適化オプションのこと、忘れてたな・・・。(-O2 かけて BoehmGC は 12.257 sec , share_ptr は 3.204 sec 。やっぱ全然駄目ジャン)

要するに、malloc のルーチン置き換えてるわけで、そうすると機種依存、OS依存なmalloc の最適化とか、かかりづらそうだよねぇ。ここは何か、もっと BoehmGC をよくみせるテストを意図的に作らなければならないはず(違)。ということで、考察。

ちなみに、ファイルサイズは、すたちっくりんくなのに 30KB 増えたくらい。

速度差2

BoehmGC に有利にするには、なるべく GC を起こさず参照カウントアクセスを頻発させることによって、解決するはず。いや、まぁループになったものも解決できる点で、BoehmGC にはデフォルトで利点はあるんですが。

というわけで、1個のオブジェクトを色々とたらいまわしにする例を考えてみる。

share_ptr 版。

// sp.cpp
// cl sp.cpp -O2 -GX -IC:\app\prog\boost_1_28_0
#include <iostream>
#include <boost/smart_ptr.hpp>
#include <boost/timer.hpp>
using namespace std;
using namespace boost;

class hoe{
  char p[0x100];
public:
  ~hoe(){
  }
  void nanika(){
    for(int i=0;i<0x100;i++){
      p[i] = i;
    }
  }
};


#define MAX_LOOP 1000000

main(){
  timer tmr;
  shared_ptr<hoe>h(new hoe);
  for(int i=0;i<MAX_LOOP;i++){
    // shared_ptr<hoe>h(new hoe); <-- typo じゃなくて、ポカです・・・ ポカ1
    shared_ptr<hoe>h1 = h;
    shared_ptr<hoe>h2 = h;
    h1->nanika();
    h2->nanika();
    // h->nanika(); <-- うわ、これもいらねーよ。ポカだぁ (T-T) ポカ2
  }
  cout << tmr.elapsed() << endl;
  return 0;
}

// 結果。6.078 sec , 6.078 sec , 6.078 sec<-- ポカ1結果です・・・。(Kent.Nさんのご指摘に拠る)

// 結果。3.064 sec , 3.074 sec , 3.064 sec. <-- ポカ2結果

正しい結果。2.082 sec , 2.072 sec , 2.072 sec駄目じゃん、全然、全然違うじゃん。

次、Boehm GC。

// t.cpp
// cl t.cpp -O2 -GX -IC:\app\prog\gc6.1alpha5\include -IC:\app\prog\boost_1_28_0 /link gc.lib user32.lib
#define GC_NOT_DLL
#include "gc_cpp.h"
#include <iostream>
#include <boost/timer.hpp>

using namespace std;
using namespace boost;

class hoe :public gc_cleanup{
  char p[0x100];
public:
  ~hoe(){
    // cout << "collected!" << endl;
  }
  void nanika(){
    for(int i=0;i<0x100;i++){
      p[i] = i;
    }
  }
};

#define MAX_LOOP 1000000

main(){
  timer tmr;
  hoe * h = new hoe;
  for(int i=0;i<MAX_LOOP;i++){
    hoe *h1 = h;
    hoe *h2 = h;
    h1->nanika();
    h2->nanika();
  }
  cout << tmr.elapsed() << endl;
  return 0;
}

結果。2.934 sec , 2.934 sec , 2.934 sec。こいつは正しい(はず)。

というわけで、流石に勝ちましたよ。やったよ BoehmGC。 <-- ポカ結果です。

全然やってないよ、負けちゃってるよ、全然負けちゃってるよ!!

もしかしたら、share_ptr の使い方間違ってるかもしれないけど・・・。うーん。 <-- 間違ってたよ。全然間違ってるよ (T-T)

でも、なんでだろう? BoehmGC が負ける要素は無さそうなんだが・・・。うーん、最適化のかかり方が違うんだろうか。全部キャッシュに入りそうだしなぁ。うーん。

なんにせよ、share_ptr って速いんだねぇ。


(ポカ後追記分)

というわけで、BoehmGC のリベンジ。もっとオーダーあげてみましょう。hoe::nanika() 内のループを(無駄な) 0x100 から 3 にまで落として、main 内のループを 1000000 から 100000000 にしてみました。すると。

となり、今度こそはっきりと Boehm GC 、勝ちました!!

でも、実はまだ、なんか落とし穴がありそうなんだけど・・・。最適化でなんか変なことしちゃったとか。なんか、結果が極端なんだよなぁ? 両方とも -O2 にそろえたし。アセンブラリストを見ないとなんともいえないか。とりあえず、今日はいいや、もう ^^;。

最適化オプション -O2 を外してコンパイルすると、(shared_ptr:62.179 / BoehmGC : 20.038) と、そんな激しくなくなりました。アセンブラリストちょっと見たけど、最適化の違い・・・なのかなぁ、判別がつかん・・・。もう少し、まともなテストしないと、真相は謎っぽい。というか、最適化でh1/h2、異なるポインタじゃないじゃん、ってーかそのまま h 使ってるし>BoehmGC 版。

一応、勝ったときのソースを。share_ptr / Boehm GC の順に。

// sp.cpp
#include <iostream>
#include <boost/smart_ptr.hpp>
#include <boost/timer.hpp>
using namespace std;
using namespace boost;

class hoe{
  char p[0x100];
public:
  ~hoe(){
  }
  void nanika(){
    for(int i=0;i<3;i++){
      p[i] = i;
    }
  }
};


#define MAX_LOOP 100000000

main(){
  timer tmr;
  shared_ptr<hoe>h(new hoe);
  for(int i=0;i<MAX_LOOP;i++){
    shared_ptr<hoe>h1 = h;
    shared_ptr<hoe>h2 = h;
    h1->nanika();
    h2->nanika();
  }
  cout << tmr.elapsed() << endl;
  return 0;
}
// t.cpp
#define GC_NOT_DLL
#include "gc_cpp.h"
#include <iostream>
#include <boost/timer.hpp>

using namespace std;
using namespace boost;

class hoe :public gc_cleanup{
  char p[0x100];
public:
  ~hoe(){
    // cout << "collected!" << endl;
  }
  void nanika(){
    for(int i=0;i<3;i++){
      p[i] = i;
    }
  }
};

#define MAX_LOOP 100000000

main(){
  timer tmr;
  hoe * h = new hoe;
  for(int i=0;i<MAX_LOOP;i++){
    hoe *h1 = h;
    hoe *h2 = h;
    h1->nanika();
    h2->nanika();
  }
  cout << tmr.elapsed() << endl;
  return 0;
}

まとめ

まず、Boehm GC を C++ で使うには。

Boehm GC を使うといいのは。

てなことがわかりました。

Linux で gcc で、ってなると、また違うのかもしれませんが。

しかし、速度差2の前半の結果が腑に落ちん・・・。

これ、違うぜ、とかあったらどんどん突っ込んでやってください。(タブン、マダ、アリソウダ>ポカ)

以上。

// あー、久しぶりにプログラムらしいプログラムした気がする。

// あ、そういえば new / delete の普通の比較、やってないなぁ。

Sasada Koichi / sasada@namikilab.tuat.ac.jp
$Date: 2003/02/26 13:07:29 $