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 ではもっといい感じになってたりするんですかね。

Ruby拡張モジュール 入門以前

こんにちわ。安定して世の中の5年ビハインドを追う hasegaw です。


Ruby というと 10年近く前に tDiary のレイアウトとかいじるために数行書き換えた程度しか経験がなくて Ruby プログラミングなんてできなかったワタクシですが、この度 Ruby で改めて Hello World いたしまして、というか Ruby は書けないけども Ruby の拡張モジュールを C(++) で実装して色々わかったことを。


■最低限の実装


拡張モジュールはC言語(もしくは後述の方法によりC++言語)にて記述できる。以下のファイルを hoge.c としてローカルディレクトリに置く。ファイル名はなんでも良い。


#include "ruby.h"
VALUE hello_func(VALUE self, VALUE s) {
        printf("hello %s\n", RSTRING_PTR(s));
        return INT2FIX(777);
}

void Init_hoge()
{
        VALUE module;
        module = rb_define_module("Hoge");
        rb_define_module_function(module, "hello",
                (hello_func), 1);
}



このファイルをビルドするためには extconf.rb ファイルを作成する。この中には以下の2行を書いておけばよい。

require 'mkmf'
create_makefile('hoge')


このうち create_makefile() の引数として指定した部分ができあがる拡張モジュールファイルの名前になる。例えば Linux
環境の Ruby であれば hoge.so だったり、 OS X 環境の Ruby であれば hoge.bundle
だったりというファイル名が出力される共有ライブラリになる。

なおコンパイル対象のファイルについての指定はしていないが、ローカルディレクトリにあるソースコードは片っ端からコンパイルするみたい。でもお作法的には "ライブラリ名.cpp" がメインのソースコード名になるようだ。

共有ライブラリを作るには extconf.rb を実行して Makefile を生成させ、その Makefile でコンパイルする。

$ ruby extconf.rb
$ make


拡張モジュールを読み込むためには  Ruby 起動時に -r"module_name" オプションを指定すればよい。たとえば下記のようなイメージでC言語側の実装を呼び出せる。

ruby -rhoge -e 'Hoge.hello("world")'
-> "hello world"


■ g++ でコンパイルしたい

Linux 上にて、参照するインクルードファイルのパースに g++ が必要だったのでソースを gcc ではなく g++
でどうしてもコンパイルしたかった。このため、当初はコンパイラを g++ に切り替えるために無理やり Makefile の CC=
の行を書き換えていたが、どうやら extconf.rb に下記の変更を加えるのが正しい?みたい。

require 'mkmf'
have_library('stdc++')    # 追加した
create_makefile('hoge')


ただ、これだけだと未だソースは gcc でコンパイルされてしまう。もう一つの作業はソースコードの拡張子を .c から .cpp に書き換える。

$ mv hoge.c hoge.cpp

■ g++ でコンパイルを通す

これで使われるコンパイラが g++ に変わったが、そうすると明示的に型キャストしないとコンパイルが通らなかったりする(OS Xのclangではそのまま通るみたい)。rb_define_module_function() の第二引数の型キャストで叱られるが、下記の要領で回避できる。

変更前
        rb_define_module_function(module, "hello",
                (hello_func), 1);


変更後
      rb_define_module_function(module, "hello",
              reinterpret_cast<VALUE(*)(...)>(hello_func), 1);


これでコンパイルは通るのだが、実際にはこのライブラリは使えない(RubyがInit_hoge関数にリンクできない)。

$ ruby -rhoge -e 'Hoge.hello("world")'
./hoge.so: ./hoge.so: undefined symbol: Init_hoge - ./hoge.so (LoadError)


悩んだ末判ったことは g++ でコンパイルしているとエクスポートされるシンボル名が gcc で C 言語としてコンパイルした場合と変わってしまうということに気づいた。

gcc でコンパイルした場合
$ objdump -t hoge.so  | grep Init_
00000000000006e0 g     F .text  000000000000002f              Init_hoge


g++ でコンパイルした場合
$ objdump -t hoge.so  | grep Init_
0000000000000760 g     F .text  000000000000002f              _Z9Init_hogev


Google 先生に聞いてみたら、これは関数を宣言するときに extern "C" と書いておけば g++ でも C 言語ルールで関数をエクスポートしてくれるらしい。 g++ でコンパイルできる拡張モジュールは最終的に下記のようなソースコードになった。(もはや Ruby じゃなくて C/C++ の話w)

#include "ruby.h"

extern "C" VALUE hello_func(VALUE self, VALUE s) {
        printf("hello %s\n", RSTRING_PTR(s));
        return INT2FIX(777);

}

extern "C" void Init_hoge()
{
VALUE module;

    module = rb_define_module("Hoge");
    rb_define_module_function(module, "hello",
            reinterpret_cast<VALUE(*)(...)>(hello_func), 1);
}


ここまでがわかれば、後は Web サイト上に様々な情報が落ちているので、もう悩むことはない。特に Ruby 上のオブジェクトをいじるときに下記が大変よいとっかかりになった。

「はじめてのRuby拡張ライブラリ」 (とみたまさひろさん)
http://www.slideshare.net/tmtm/ruby-ext

でRubyの世界とどう干渉すればいいかはわかるし、その先で何か困ったらとりあえず Ruby についている README.EXT を見ればよい。