京都収納棚爪哇束縛及同錦蛇束縛

ID: 78
creation date: 2010/05/13 19:49
modification date: 2010/05/13 19:49
owner: mikio

やるといったらやる男の道はさらに続き、Kyoto CabinetのJavaバインディングとPythonバインディングを前倒しで仕上げた。

Javaバインディング

JDK1.5以降で言語仕様がだいぶ変わったようなのでびびっていたのだが、DBMのバインディングを書くというくらいでは新しい仕様に合わせてどうのこうのする必要はほとんどなかったらしく、すんなりできた。JNIのインターフェイスも全く変わっていないし、コンテナのジェネリクスに関してもコンパイラの層で解決してくれるのでJNI側では単にObjectの授受をするように書いておけばOKなようだ。

Visitorパターン風インターフェイスのサンプルコードはこんな感じになる。

import kyotocabinet.*;

public class KCDBEX2 {
  public static void main(String[] args) {

    // create the object
    DB db = new DB();

    // open the database
    if (!db.open("casket.kch", DB.OREADER)) {
      System.err.println("open error: " + db.error());
    }

    // define the visitor
    class VisitorImpl implements Visitor {
      public byte[] visit_full(byte[] key, byte[] value) {
        System.out.println(new String(key) + ":" + new String(value));
        return NOP;
      }
      public byte[] visit_empty(byte[] key) {
        System.err.println(new String(key) + " is missing");
        return NOP;
      }
    }
    Visitor visitor = new VisitorImpl();

    // retrieve a record with visitor
    if (!db.accept("foo".getBytes(), visitor, false) ||
        !db.accept("dummy".getBytes(), visitor, false)) {
      System.err.println("accept error: " + db.error());
    }

    // traverse records with visitor
    if (!db.iterate(visitor, false)) {
      System.err.println("iterate error: " + db.error());
    }

    // close the database
    if(!db.close()){
      System.err.println("close error: " + db.error());
    }

  }
}

Rubyでは文字の列としての「文字列」とバイトの配列としての「バイト配列」を同じクラスで表現していたが、Javaでは両者はStringおよびbyte[]という別のクラスとして表現されるので、該当するクラスの授受を行うメソッドにはオーバーロードが施してある。すなわち boolean set(byte[] key, byte[] value); には対応するラッパーとして boolean set(String key, String value); がある。ラッパーが文字列からバイト列へのエンコードを行う際や逆にバイト列から文字列へのデコードを行う際には、デフォルトではUTF-8とみなして変換が行われる。このコーディングルールを変えるにはDBクラスのset_encodingメソッドを呼ぶとよい。

あとはまあ特に技術的な課題はなかった。JavaVMはスレッドセーフだしJavaスレッドはネイティブスレッドと1対1対応なのでKCのスレッド周辺機能もちゃんと動作するし、GCなんでメモリリークもそんなに気にしなくていい。もちろんネイティブオブジェクトの回収忘れだけは入念にチェックした。

API文書はもちろんドキュメンテーションコメントで書いてJavaDocに整形させることで生成した。

Pythonバインディング

俺はPythonを書いたことがない。でもPythonに習熟してからバインディングを書くのは面倒だったので、いきなりC側のAPIを調べてCだけの視点でバインディングを書くことにした。Python本家の文書は非常に充実していて、Extending and Embedding the Python Interpreterの章とPython/C API Reference Manualの章を読めばバインディングが書けるようになっている。事実、ちゃんと書けた。もちろんテストドライバはPythonで書いたわけだが、C側を理解しているとPython側の理解もかなり楽だったのでそちらもすんなり書けた。

Visitorパターン風インターフェイスのサンプルコードはこんな感じになる。

from kyotocabinet import *
import sys

# create the database object
db = DB()

# open the database
if not db.open("casket.kch", DB.OREADER):
    print("open error: " + str(db.error()), file=sys.stderr)

# define the visitor
class VisitorImpl(Visitor):
    # call back function for an existing record
    def visit_full(self, key, value):
        print("{}:{}".format(key.decode(), value.decode()))
        return self.NOP
    # call back function for an empty record space
    def visit_empty(self, key):
        print("{} is missing".format(key.decode()), file=sys.stderr)
        return self.NOP
visitor = VisitorImpl()

# retrieve a record with visitor
if not db.accept("foo", visitor, False) or \
        not db.accept("dummy", visitor, False):
    print("accept error: " + str(db.error()), file=sys.stderr)

# traverse records with visitor
if not db.iterate(visitor, False):
    print("iterate error: " + str(db.error()), file=sys.stderr)

# close the database
if not db.close():
    print("close error: " + str(db.error()), file=sys.stderr)

上記で用いている共通IDLのインターフェイスはもちろんのこと、Pythonの演算子をオーバーライドすべく __getitem__, __setitem__, __iter__あたりも実装してあるので、Python組み込みの連想配列(ディクショナリ)のように使うこともできる。もちろんforループも回せる。さらに、関数オブジェクトを渡してVisitorパターンっぽく使うこともできる。結構便利じゃね?

from kyotocabinet import *
import sys

# define the functor
def dbproc(db):

  # store records
  db[b'bar'] = b'step';  # bytes is fundamental
  db['foo'] = 'hop';     # string is also ok
  db[3] = 'jump';        # number is also ok

  # retrieve a record value
  print("{}".format(db['foo'].decode()))

  # update records in transaction
  def tranproc():
      db['foo'] = 2.71828
      return True
  db.transaction(tranproc)

  # multiply a record value
  def mulproc(key, value):
      return float(value) * 2
  db.accept('foo', mulproc)

  # traverse records by iterator
  for key in db:
      print("{}:{}".format(key.decode(), db[key].decode()))

  # upcase values by iterator
  def upproc(key, value):
      return value.upper()
  db.iterate(upproc)

  # traverse records by cursor
  def curproc(cur):
      cur.jump()
      def printproc(key, value):
          print("{}:{}".format(key.decode(), value.decode()))
          return Visitor.NOP
      while cur.accept(printproc):
          cur.step()
  db.cursor_process(curproc)

# process the database by the functor
DB.process(dbproc, 'casket.kch')

PythonもJavaと同じく文字列(Unicode型)とバイト配列(Bytes型)が分離されているので、両者を受け付けるようにオーバーロードしてある。といってもPythonは関数シグネチャとして型チェックをしない言語なので、動的に型を判断して、引数が文字列であればバイト列にエンコードするようになっている。戻り値を暗黙的にデコードするためには、get_strのように、_strが接尾した名前の関数を呼べばよい。エンコーディングは現状ではUTF-8固定にしている。

Pythonでは2つの課題があった。ひとつは、Rubyと同じくAPI呼び出しにGIL(Global Interpreter Lock)を必要とする処理系なので、それに対応すべく自前でロックを再実装したり、直列モード(デフォルト)と並列モード(DBのコンストラクタにGCONCURRENTオプションをつける)を分離したりといった対策を施す必要があったということ。もうひとつは、Pythonオブジェクトの寿命管理がリファレンスカウント方式なので、C側では細心の注意を払いながらカウントを上げ下げしないといけないということ。まあ両方とも頑張れば何とかなる問題なので何とかしたけども。

API文書はドキュメンテーションコメントで書いてPyDocで整形することを考えていたが、PyDocのHTML出力はあまりにかっこ悪いので今のところDoxygenで代用している。Sphinxを使おうとも思ったが、ドキュメンテーションコメントからreStructuredTextデータを自動生成する方法がよくわからなかったので、誰かこっそり教えてくれると嬉しい。

ちなみに、現状ではPython3のみの対応で、Python2.x系ではビルドも利用もできない。2.x系の対応いずれユーザが付きそうならRuby1.8対応とともに考えるだろう。

性能

JavaとPythonとRubyの各バインディングで性能比較をしてみた。ファイルハッシュデータベースを対象とした合計100万レコードの書き込みと同じ数の読み込みを、スレッド数を変えながらやってみる。CPUはIntel Xeon E5345でメモリは8GBでOSはUbuntu 10.4(Linux 2.6.32)でコンパイラはGCC 4.4.3。Javaは1.6.0_20で、Pythonは3.1.2で、Rubyは1.9.1p378。

write read
Java(1スレッド) 7.109 6.836
Java(2スレッド) 4.476 3.812
Java(4スレッド) 2.889 2.267
Python(1スレッド) 5.297 5.048
Python(2スレッド) 13.502 10.997
Python(4スレッド) 18.070 18.237
Ruby(1スレッド) 4.142 4.352
Ruby(2スレッド) 4.101 4.082
Ruby(4スレッド) 3.918 3.942

上記で、Javaはスレッド数の応じて性能が上がっているのがわかる。一方で、RubyとPythonは、並列化によって性能は向上させられないこともわかる。PythonとRubyではGILを外さずにKCのネイティブ処理を呼んでいるので、1スレッドしか同時に動作しないのは当然である。Pythonではスレッド数の増加に応じて逆に性能が下がっているのだが、おそらくDB::accept等で呼ばれるコールバックを保護するためのPythonレベルのロックの負荷とスレッドのコンテキストスイッチの負荷が合わさって性能劣化を招いているのだろう。Rubyでも同じようにロックしているのだが、性能が劣化していないのは不思議だ。

では、GILを外してロックもしない並列モードだとどうなるか。同じテストをやってみた。

write read
Python(1スレッド) 4.027 3.834
Python(2スレッド) 7.333 6.656
Python(4スレッド) 9.034 4.003
Ruby(1スレッド) 3.909 4.099
Ruby(2スレッド) 7.938 7.094
Ruby(4スレッド) 6.999 7.561

元々マルチスレッドだとすげー遅かったPythonでは性能がかなり改善されたが、それでもシングルスレッドの方が早いという事実は変わらなかった。Rubyでは逆に遅くなった。やはりKCが早すぎる(同じテストをC++でやると1秒もかからない)ので、その部分を並列化しても全体の性能は上がらないということか。

まとめ

いろいろ苦労しつつも、KCのJavaバインディングとPythonバインディングとRubyバインディングが出揃った。それぞれに共通したインターフェイスと言語ならではのオマケ機能がついて、便利につかえると思う。また、マルチスレッド環境でもきちんと動くことがわかった。KCの負荷が小さい状態だとJava以外では並列化の恩恵はあまりないのだが、DBが大きくなって負荷が増大した場合には並列化の恩恵が得られるとは思う。

さて、次はLuaとPerlか。つか、なんかもう「処理系のAPIを叩くだけの簡単なお仕事」には飽きてきた感もあるな。精密な実装力とデバッグ力の修行にはなるけども、新しいことをやっている感覚がない。ちょっと休憩して別のことやろうかなぁ。でも、やるといったらやる男だしなぁ。

8 comments
mattn : mingw32でビルドしてみました。1点キャストエラーの修正(patch: http://gist.github.com/415364) と、mingw32用の Makefile(非msys: http://gist.github.com/415361) でビルドできました。 (2010/05/27 11:29)
mattn : あ、すみません。kyotocabinet-1.0.0のpatchでしたが、諸事情によりメールが使えなかったのでこちらにて報告させて頂きました。 (2010/05/27 11:30)
mikio : ご報告ありがとうございます。参考にさせていただきます。 (2010/05/27 14:12)
nobutaka : パッチもなく報告だけで申し訳ないのですが、JavaバインディングをclojureとREPLという組み合わせで使っていると(new DB).toString()のようなことをついやってしまいます。今のコードだとopenしてないとtoStringでnull pointer exceptionがあがってくるので、最初なにかバインディングがうまく動いていないのかと勘違いしてしまいました。いつtoStringしても平気になるとうれしいなぁ、なんて。動作自体は (2010/06/01 12:42)
nobutaka : 動作自体は問題ないようです。(すみません途切れちゃいました) (2010/06/01 12:44)
mikio : ありがとうございます。近いうちに対策いたします。 (2010/06/02 00:35)
akira : kyotocabinet-1.0.0をVC2010でビルドできました。 添付(kctreedb)のサンプルとリンクする場合に「kyotocabinet.lib(kcdb.obj) : error LNK2038: '_ITERATOR_DEBUG_LEVEL' の不一致が検出されました。値 '0' が 2 の値 ' と一致しません。」が多く出ます。どこを直したらいいか教えてください。 (2010/06/29 18:08)
mikio : VSの細かい仕様は実は知らないのですが、多分、ビルドオプションとかで _ITERATOR_DEBUG_LEVEL を一致させればいいんじゃないでしょうか。 (2010/06/30 08:43)
riddle for guest comment authorization:
Where is the capital city of Japan? ...