2013年10月26日土曜日

Ruby拡張モジュール マルチスレッドでの性能向上

Ruby の VM は giant lock の塊で、スレッドといっても実行できる Ruby VM
のコンテクストはひとつだけで、それを時分割多重で動かすのがふるーい Ruby のスレッドだったらしい。なるほど Ruby
のスレッドの性能が低いって言われていたのはこれのことだったのね。リンクするライブラリの都合上 CentOS 6
で作業を行っていたので、そのまま標準の Ruby 1.8 を使っている限りこれはどうにもならないんだけど、 Ruby 1.9
へアップグレードすると、 Ruby が管理しているオブジェクトを触らない前提で giant lock を解放できる仕組みがあるらしい。 Ruby
のマニュアルには下記のように書いてある。


ネイティブスレッドを用いて実装されていますが、 現在の実装では Ruby VM は Giant VM lock (GVL)
を有しており、同時に実行される ネイティブスレッドは常にひとつです。 ただし、IO 関連のブロックする可能性があるシステムコールを行う場合には
GVL を解放します。その場合にはスレッドは同時に実行され得ます。 また拡張ライブラリから GVL を操作できるので、複数のスレッドを
同時に実行するような拡張ライブラリは作成可能です。



つまり、やはりどう頑張っても Ruby VM は 1スレッドでサチってしまうので Ruby で長時間かかるような計算は向かないけども、
pthread な Ruby を使う前提であれば長時間かかる処理を Ruby VM
の外側でやったりとか(ない)、IO等など外部で大量に時間を使うようなケースなら別のスレッドとして Ruby VM
を走らせたり、そこから並列で拡張モジュール側のコードを読んだりはできる、ということだよね。調べていたら Ruby 1.9 の YARV では
rb_thread_blocking_region() という関数を介して関数呼び出しをしている間は GVL が解放されるらしい。


■ rb_thread_blocking_region() の導入

struct nogvl_bubi_args {
    int a;
    int b;
    int c;
};

int nogvl_bubi(nogvl_bubi_args *args) {
        return bubi(args->a, args->b, args->c);
};


としておいて、呼び出し元を

struct nogvl_bubi_args args;
args.a = 1;
args.b = 2;
args.c = 3;

#ifdef HAVE_RB_THREAD_BLOCKING_REGION
        r = rb_thread_blocking_region(
  (rb_blocking_function_t*) nogvl_bubi,
       
 &args, NULL, NULL);
#else
        r = nogvl_bubi(&args);
#endif


としてみた。

■ extconf.rb の修正

HAVE_RB_THREAD_BLOCKING_REGION はどこで定義されているのだろうと思ったら、これは extconf.rb に書いてあげないと定義されないことに気づいた。 extconf.rb に以下の行を追加する。

have_func('rb_thread_blocking_region')

こうすると extconf.rb 実行時に rb_thread_blocking_region があるかないか確認して、あれば HAVE_RB_THREAD_BLOCKING_REGION が使えるということのようだ。

■ どれぐらい変わるか?

Red Hat Software Collection から ruby193 をインストールして今作っているRubyモジュールの動作速度を比較してみたところこんなかんじだった。

Ruby 1.9 (RB_THREAD_BLOCKING_REGIONを使う場合 113.657433s
Ruby 1.9 (RB_THREAD_BLOCKING_REGIONを使わない場合 196.696434s

Perl でも XS で同等機能のモジュールを書いているんだけど、 Perl の場合はスレッドセーフに書かれているみたいで何も気にする必要はなかった。 Ruby 2.0 ではもっといい感じになってたりするんですかね。

0 件のコメント:

コメントを投稿