読者です 読者をやめる 読者になる 読者になる

MeCabのメモリ管理はどうなっているのか

以前mecab-rubyを用いた下記のコードがコケる場合がありました。

node = MeCab::Tagger.new.parse(text)

これはparseメソッドを呼び出している最中にMeCab::TaggerインスタンスがGCによって解放されてしまい、メモリ違反を起こすためです(昔のことなのであやふやですが)。なので、parseメソッドに与える文字列の長さが短い場合はエラーが発生せず、長ければ長いほど発生し易いといういやらしいものです。

まあ今手元のmecab-rubyMeCabのバージョンは0.996)で試したら'a' * 100_000な文字列を与えてもそんな問題は発生しませんでしたけどね…… おかしいな……

そもそもなんでこんな話を思い出したかと言うと、MeCabのメモリがどういう風に管理されているかが気になったからです。

MeCabは0.99になってからModelとLatticeという概念が導入され、マルチスレッド時における使用法が変わりました。Modelはこの問題によって存在を知りましたが、実を言うとLatticeは知りませんでした。で、さきほどLatticeが気になりだしたのですが、なぜLatticeはスレッド毎に生成しなければならないのか、あれ、メモリはどうなってるんだろう、と次々と疑問が湧いたのでMeCabソースコードで確認した次第です。

それのレポートがこちら。

なおMeCabのバージョンは最新の0.996です(ソースコード)。調べた対象のファイルはsrc/tagger.cppです。

Tagger

MeCabは基本Taggerを基点にし、それのメンバ関数を呼び出すことによって形態素解析を行なっています。なので、まずはTaggerを見ていきます。Taggerの実装はTaggerImplに詰まっておりますので、そこを。

class TaggerImpl: public Tagger {
 public:
  bool                  open(int argc, char **argv);
  bool                  open(const char *arg);
  bool                  open(const ModelImpl &model);

  bool                  parse(Lattice *lattice) const;

  void                  set_request_type(int request_type);
  int                   request_type() const;

  const char*           parse(const char*);
  const char*           parse(const char*, size_t);
  const char*           parse(const char*, size_t, char*, size_t);

  /* 省略 */

  Lattice *mutable_lattice() {
    if (!lattice_.get()) {
      lattice_.reset(model()->createLattice());
    }
    return lattice_.get();
  }

  const ModelImpl          *current_model_;
  scoped_ptr<ModelImpl>     model_;
  scoped_ptr<Lattice>       lattice_;
  int                       request_type_;
  double                    theta_;
  std::string               what_;
};

ちょっと省略しましたが、こんな感じです。見てみると、メンバ変数にmodel_とlattice_がありますね。それぞれscoped_ptrで定義されています。このscoped_ptr、boostのそれを利用しておらず、src/scoped_ptr.cppにて下記のように定義されております。

template<class T> class scoped_ptr {
 private:
  T * ptr_;
  scoped_ptr(scoped_ptr const &);
  scoped_ptr & operator= (scoped_ptr const &);
  typedef scoped_ptr<T> this_type;

 public:
  typedef T element_type;
  explicit scoped_ptr(T * p = 0): ptr_(p) {}
  virtual ~scoped_ptr() { delete ptr_; }
  void reset(T * p = 0) {
    delete ptr_;
    ptr_ = p;
  }
  T & operator*() const   { return *ptr_; }
  T * operator->() const  { return ptr_;  }
  T * get() const         { return ptr_;  }
};

デストラクタでptr_メンバ変数を解放しているので、TaggerImplの開放時にmodel_メンバ変数もlattice_メンバ変数も解放されそうです。

Taggerが解放されるとModelが解放されてしまうが良いのか?

Modelを用いない場合(シングルスレッドで利用する場合)はそれで問題ありません。ただ、Modelを利用する場合はどう考えても良くありませんよね。ですので、仕掛けがあるはずです。まあどう見てもcurrent_model_メンバ変数が匂いますね。実際は下記のようになっています。

まずはTaggerをコマンドラインに与える文字列で初期化した場合に呼ばれる関数。これはModelを用いない場合に該当します。

bool TaggerImpl::open(const char *arg) {
  model_.reset(new ModelImpl);
  if (!model_->open(arg)) {
    model_.reset(0);
    return false;
  }
  current_model_ = model_.get();
  request_type_ = model()->request_type();
  theta_        = model()->theta();
  return true;
}

model_メンバ変数に新しいModelImplを設定し、そのポインタをcurrent_model_メンバ変数に設定しています。Modelを用いない場合はModelに触れることなくTaggerを扱うため、これで問題ありません。

次にModelからTaggerを作成する場合。こちらがそのままでは問題となるケースです。以下はModelを用いた場合に呼ばれる関数。

bool TaggerImpl::open(const ModelImpl &model) {
  if (!model.is_available()) {
    return false;
  }
  model_.reset(0);
  current_model_ = &model;
  request_type_ = current_model_->request_type();
  theta_        = current_model_->theta();
  return true;
}

model_メンバ変数にNULLを設定するだけで、後は直接current_model_メンバ変数に引数(Taggerの作成に使用したModel)を設定しています。current_model_メンバ変数はポインタですので、Taggerのデストラクタで解放されません。

current_model_メンバ変数をクッションにして直接Taggerを得ても、ModelからTaggerを得ても大丈夫なようにしているようです。

Taggerが解放されてしまうとLatticeが解放されてしまうけど良いのか?

そもそもLatticeとは何なのか。こちらを参照

Latticeオプジェクトは解析に必要なすべてのローカル変数を含んでいます。必ずスレッド毎に1つのオブジェクトを作成してください

MeCab: Yet Another Japanese Dependency Structure Analyzer

うん、よく分かりません。解析にあたってのコンテキストみたいなものでしょうか? そしてなぜスレッド毎に1つなのか。うーん。

とりあえずそのLatticeがlattice_メンバ変数に設定されるみたいです。ではどこで使われているか。

  Lattice *mutable_lattice() {
    if (!lattice_.get()) {
      lattice_.reset(model()->createLattice());
    }
    return lattice_.get();
  }

こちらは先述のTaggerImplの定義の一片ですが、mutable_latticeというメンバ関数中でlattice_は使われています。lattice_にまだポインタが設定されていなかったらcreateLatticeで作成したLatticeのインスタンスを設定し、以後それを返すようですね。Rubyで書くと:

def mutable_lattice
  @lattice ||= model.create_lattice
end

って感じでしょうか。

ではではそのmutable_latticeはどこで呼ばれているか、いきなり核を付きそうなparseメンバ関数にて使われています。

const char *TaggerImpl::parse(const char *str, size_t len) {
  Lattice *lattice = mutable_lattice();
  lattice->set_sentence(str, len);
  initRequestType();
  if (!parse(lattice)) {
    set_what(lattice->what());
    return 0;
  }
  const char *result = lattice->toString();
  if (!result) {
    set_what(lattice->what());
    return 0;
  }
  return result;
}

parseはTaggerが持つLatticeに対して処理を行うようですね。実際parse(const char*, size_t)はparse(MeCab::Lattice*)を呼んでいます(上から5行目)。その前にLatticeのset_sentenceメンバ関数を呼んでいます。そしてparse(MeCab::Lattice*)の呼び出しの後にtoStringメンバ関数で処理の結果を得ています。これが「解析に必要なすべてのローカル変数を含んでい」るということなんでしょう。

そしてもうひとつ。「必ずスレッド毎に1つのオブジェクトを作成してください」とは。これ、どう見てもTaggerとLatticeが1:1で結び付いていますね。なのでスレッド間でTaggerを共有した場合、タイミングによっては処理結果の上書きが発生しそうです。たしかにスレッド毎にTaggerを作る必要はありそうです。またparse(MeCab::Lattice*)によって外部からLatticeを与えることが出来るので、その与えるLatticeをスレッド間で共有しても同様の結果が起きそうです。こちらもスレッド毎にLatticeを作る必要がある、というのに充分な理由ですね。

となると、TaggerかLattice、どちらかをスレッド毎に作成すればいいのでは、という気もしてきます。しかもLatticeをparse関数に直接与えるのであれば:

model = MeCab::Model.create('')
lattice = model.createLattice
model.createTagger.parse(lattice)

として、TaggerがGCによって回収されても何ら問題ないのでは? なぜなら処理結果を格納しているLatticeは外側に持っているのだから。そんな気がしますね。

ちなみに0.98ではどうなっているのか

同じくtagger.cppなのですが、Modelで検索してもLatticeで検索してもヒットしません。やはり0.99からのものなんですね。で、下記が0.98のparse関数。

const char *TaggerImpl::parse(const char *str, size_t len) {
  const Node *n = parseToNode(str, len);
  if (!n) return 0;
  ostrs_.clear();
  CHECK_0(writer_.write(&ostrs_, str, n)) << writer_.what();
  ostrs_ << '\0';
  return ostrs_.str();
}

ostrs_というメンバ関数に解析の結果を書き込んでいます。ちなみにostrs_は:

  StringBuffer               ostrs_;

という風に宣言されているので、Tagger死亡と同時に解放されます。うん、先頭で書いた例がコケそうだと改めて思いました。まあ0.996でもlattice_に格納しているだけの差なので、コケないとおかしいよなあとは思いますが…… むしろ0.98ではparseToNodeの結果はTaggerのメンバとして扱わないので、問題ないのでは…… 調べ切ったように見えましたが、微妙に疑問が残ります。

まとめ

とりあえず0.98のことは忘れて*1、0.99 laterの場合なのですが、基本:

  1. シングルスレッドではTaggerを直接生成、マルチスレッドではModelを生成し、そこからTaggerをスレッド毎に生成
  2. parse(const char*)は使わず、parse(MeCab::Lattice*)を使い、mutable_latticeには手を触れない
  3. parseに与えるLatticeもTaggerと同じくスレッド毎に生成

の3点に気を付ければかなり安全だと思います。

*1:未だにapt-getなどのパッケージマネージャで入れるとインストールされますが……