やるといったらやる男の道はさらに続き、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を叩くだけの簡単なお仕事」には飽きてきた感もあるな。精密な実装力とデバッグ力の修行にはなるけども、新しいことをやっている感覚がない。ちょっと休憩して別のことやろうかなぁ。でも、やるといったらやる男だしなぁ。