豪鬼メモ

一瞬千撃

Javaインターフェイスで2倍の高速化

TkrzwのJavaインターフェイスがGoインターフェイスの半分の性能しか出ないというのは、さすがに遅すぎるだろうという話があった。JNIのオーバーヘッドが高いからというのを遅い理由として挙げていたが、うまく書けばそれを下げられることが分かったので、やってみた。結果として、JavaインターフェイスをGoインターフェイスと互角の性能に引き上げることができた。
f:id:fridaynight:20210801224401p:plain

Read more

Python/Rubyの非同期APIとコルーチン

PythonRubyではマルチスレッドを使うと性能がむしろ悪化する。そんな話を以前の記事で書いた。グラフに現れているように、1スレッドだとGoと同じくらいの性能なのに、マルチスレッドにするとスループットが激減してしまうのだ。ところで、ゲームやその他の対話的なアプリケーションでは、並行処理(並列処理でなく)のためにコルーチンとかファイバーとか呼ばれる機構を使うことが多い。そこでの利便性を考えて、Tkrzwでも非同期APIを実装してみた。
f:id:fridaynight:20210724153934p:plain

Read more

TkrzwのGo言語インターフェイスをリリース

並列処理性能を重視したデータベースライブラリTkrzwだが、ついにGo言語をサポートした。並列処理が簡単に書けるGo言語と並列に読み書きできるDBMの親和性は高い。ここでは、各言語の性能比較をした上で、Goでの簡単な使い方について説明する。
f:id:fridaynight:20210724153934p:plain

Read more

Go言語勉強中

TkrzwのGo言語インターフェイスを鋭意製作中であるが、今更ながら、「プログラミング言語Go」を読んだ。20年前に愛読した「プログラミング言語C」や「プログラミング作法」や「UNIXプログラミング環境」のカーニハン先生が著者の一人なので、なんか懐かしい気分になった。Go言語の概要についてはこれで理解したのだが、その本で詳述されていなかった点でいくつかはまったので、メモしておく。

GoからC++言語のライブラリを直接呼ぶことはできないが、C言語の関数は直接呼ぶことができる。そのためにはcgoという機能を使う。本家のチュートリアルを読めば概要はつかめる。ユーザの環境に自動的にインストールできるようにするためには、"#cgo pkg-config" により、pkg-config経由でコンパイルやリンクのオプションが参照できるようにしておかねばならない。

Goオブジェクトのポインタをuintptrにキャストした場合、そのポインタは別の行でポインタとして使ってはならない。なぜなら、Go処理系のGCが該当のオブジェクトのアドレスを変更した際には、それを指すポインタも自動で更新してくれるが、uintptrの値はポインタではなく数値とみなされるので、GCによって更新されず、無効なポインタになるからだ。同じ行で使い切るなら良い。

一方、Cオブジェクトのポインタ(*FooBarやunsafe.Pointer)は、ポインタ型として保持し続けてはならない。Go処理系のGCは生きているゴルーチンのスタックにあるポインタを起点として参照可能なポインタを検索するので、Cオブジェクトのポインタを保持していると無駄な検索が発生するからだ。よって、Cオブジェクトのポインタは速やかにuintptrに変換して数値として保持すべきだ。

Cのスレッドローカル変数に複数のゴルーチンからアクセスすると、レースコンディションが起きる。単一のネイティブスレッド上で複数のゴルーチンがタイムシェアで切り替わって実行されるので、スレッドローカル変数の値を設定する関数呼び出しとその値を参照する関数がGoの個別の命令として書かれていると、その間にプリエンプションが起こり、別の変数設定が割り込むかもしれないからだ。それを防ぐには、スレッドローカル変数の設定とその参照は同一のC関数内で完結せねばならない。

Cで作った配列にGoからアクセスするのが大変だ。Cの配列は先頭要素のポインタと要素数で管理され、各要素の参照は先頭ポインタにオフセットを足して行われる。オフセットの算出の際には添字に要素サイズを掛け算する暗黙の変換が行われる。一方でGoではポインタの演算が許されておらず、Cオブジェクトのポインタでもそれは例外ではない。じゃあどうするかと言うと、unsafe.Pointerを介してuintptrに変換して、数値としてアドレスの演算を行う。uintptrは単なる数値であり、元のオブジェクトのサイズを知らないから、添字に要素サイズを掛け算する演算はプログラマの責任になる。そういうわけで、RecordというCの型の配列を全走査するには、以下のようなGoコードを書くことになる。

records, num_records := C.get_records(...)
rec_ptr := uintptr(unsafe.Pointer(records))
for i := C.int32_t(0); i < num_records; i++ {
  record := (*C.TkrzwKeyValuePair)(unsafe.Pointer(rec_ptr))
  ...  
  rec_ptr += unsafe.Sizeof(C.Record{})
}

新しめのGoのパッケージはGo Modulesという機能で管理される。パッケージをGitHub等の公開リポジトリに置いておけば、ビルド時に勝手にダウンロードしてインストールされるようになる。めちゃ便利だ。本家のチュートリアルを読めばだいたいのことが分かる。開発中のパッケージをパッケージの外から利用したい場合、go mod edit -replace github/estraier/tkrzw-go=../tkrzw-go のようなコマンドでgo.modファイルを書き換えて、ローカルファイルシステムを参照するシンボリックリンク的な措置をすると良い。ここで注意すべきは、GitHubにpushしたパッケージを使って何かする場合、自動ダウンロードされるバージョンが古い可能性があるということだ。参照するバージョンを@latestなどにしても、やはり古いものがダウンロードされることがある。プロクシ上にキャッシュされたパッケージのデータを取り込むからだ。それを回避するには、GOPRIVATE="github.com/estraier" などと環境変数を設定して、その接頭辞を持つパッケージをキャッシュから除外する必要がある。

現状のGoエコシステムでは、APIの文書化に関する標準的な手順が確立されていないっぽい。各種識別子につけるべきdoc commentのスタイルに関しては本家にガイドラインがあり、go doc Hogeなどとして閲覧する機能も整備されている。一方で、HTMLなどの構造化データを書き出す標準的な手段はない。godocという外部ツールがあり、Ubuntuだとgolang-golang-x-toolsパッケージとしてインストールできるが、そいつはWebブラウザでHTML文書を閲覧するためのWebサーバとして機能するが、HTML文書の書き出しはしてくれない。かつてはその機能があったが、メンテ上の理由で削除されたらしい。「CLIの方が慣れれば便利だよ」「毎回Webサーバを立てればいいよ」「pkg.go.devにアップロードすれば自分でWebサーバを立てなくてもよくなるよ」といった意見が巷にはあるらしいが、自分には腑に落ちない。docというモジュールのToHTMLメソッドで生成できるっぽい気もするが、あんまり使っている人がいない。

「godocがサーブするHTMLをwgetcurlでダウンロードする」という作戦の人達もいて、今の所それが最も現実的だと判断した。Makefileに以下のようなことを書いておいて、make apidocを実行すると、API文書のHTMLファイルがディレクトapi-docの中に作られるようにした。JavaScriptの無効化やCSSの修正をするとともに、「-」で始まる行をリストの項目として扱うようにもしている。

apidoc :
  rm -rf api-doc
  godoc -http "localhost:8080" -play & sleep 1
  mkdir api-doc
  curl -s "http://localhost:8080/lib/godoc/style.css" > api-doc/style.css
  echo '#topbar { display: none; }' >> api-doc/style.css
  echo '#short-nav, #pkg-subdirectories, .pkg-dir { display: none; }' >> api-doc/style.css
  echo '.list { display: list-item; list-style: circle outside; }' >> api-doc/style.css
  echo '.list { margin-left: 4.5ex; }' >> api-doc/style.css
  curl -s "http://localhost:8080/pkg/github.com/estraier/tkrzw-go/" |\
    grep -v '^<script.*</script>$$' |\
    sed -e 's/\/[a-z\/]*style.css/style.css/' \
      -e 's/\/pkg\/builtin\/#/#/' \
      -e 's/^- \(.*\)/<div class="list">\1<\/div>/' > api-doc/index.html
  killall godoc

Go言語はC言語の文法をちょっと変えて並列処理がしやすいように計らった言語という印象だ。ゴルーチンで並列処理が簡単にできるようになり、チャンネルによって並列環境での同期処理が簡単にできるようになり、ガベージコレクションによってリソース管理が簡単にできるようになっている。それ以外は、ポインタがあったり、構造体やその他のオブジェクトのサイズが取得できたりなど、めちゃC言語っぽい。C言語のノリの低水準を保ちつつ、メモリ関連のバグが起きないように各種の配慮がなされている。型チェックがやたら厳密なのが特徴的で、intとint32の間で暗黙のキャストがなされないくらい。当然、[]byteとstringは異なる型なので、相互に明示的な変換が必要だ。DBMは任意のバイト列の連想配列を格納する機能を提供するが、実際には文字列を格納するユースケースが多い。よって、PythonRubyのように文字列でもバイト列でも扱えるAPIを提供したいが、そのためには空インターフェイスとインフレクションを併用すればよい。以下のようなサンプルコードになるはずだ。

// データベースを作って開き、勝手に閉じるようにする
dbm := tkrzw.NewDBM()
dbm.Open("casket.tkh", true, "truncate=true")
defer dbm.Close()

// 文字列のキーと文字列の値を格納。暗黙的にバイト列に変換される。
dbm.Set("japan", "tokyo")

// バイト列をそのまま格納
dbm.Set([]byte("china"), []byte("beijing"))

// 文字列で検索してバイト列を取得。文字列に変換してから印字。
raw_value, status := dbm.Get("japan")
if status.IsOK() {
  fmt.Println(string(raw_value))
}

// バイト列で検索して文字列を取得。そのまま印字。
str_value, status := dbm.GetStr("china")
if status.IsOK() {
  fmt.Println(str_value)
}

APIの細かい仕様はまさに今実装しながら詰めているところだが、おそらく近日中に完了して公開できると思う。今気づいたが、ローカル変数をスネークケースで書く癖がついていた。全部キャメルケースに直さにゃならんのか。

非同期APIの性能評価

非同期APIでは、該当の処理をバックグラウンドで実行するためにスレッドを使う。と同時に、バッチ処理やサーバプログラムなどではフォアグラウンドに相当するスレッドが複数個同時に実行される。となると、性能評価はフォアグラウンドのスレッド数とバックグラウンドのスレッド数の二次元でなされねばならない。その結果が以下である。
f:id:fridaynight:20210717152259p:plain

Read more

非同期APIでデータベースを操作する

データベースライブラリの既存の同期的なAPIをラップして、非同期のAPIを作ったという話。C++のstd::futureクラスと独自のスレッドプールを使っている。std::futureのおかげで、スレッドプログラミングの知識が全く無いプログラマでも簡単に非同期処理機能を利用できる。スレッドプールは隠蔽されているが、おかげで低いオーバーヘッドで非同期処理が実現できる。この機能はTkrzw-0.9.43から利用できる。
f:id:fridaynight:20210715221112p:plain

Read more

C++標準async+futureとスレッドプールの性能比較

C++11標準以降のasyncとfutureによる非同期処理は非常に使いやすく、とても簡単に非同期処理を実装することができる。それによって、マルチスレッドでは複数の処理を並列実行できた場合には、スループットが向上させられる。一方で、単一の処理をシングルスレッドで処理する場合には、スレッドを作ったりスレッド間のコンテキストスイッチをしたりするのにかかるオーバーヘッドで、スループットは低下する。それについて性能評価をした。スレッドプールを実装して、それとの比較も行った。
f:id:fridaynight:20210714165334p:plain

Read more

C++/Java/Ruby/Pythonの並列I/O用ファイルインターフェイス

ファイルの読み書きをマルチスレッドで並列に行うための仕組みを、各種プログラミング言語C++、C、JavaRubyPython)で利用できるようにライブラリを整備した。メモリマップI/O、通常I/O、ダイレクトI/Oを同じインターフェイスで利用できる。Tkrzw-0.9.38からこの機能が利用できる。メモリマップI/Oと通常I/Oは全ての処理系で、ダイレクトI/OはLinuxMacWindowsで利用できる。

Read more

TkrzwのC言語インターフェイス

C++言語で書かれているデータベースライブラリTkrzwは、当然ながらC++APIを提供するが、C言語からは使えない。なので、主要機能をクラスでない関数でラップしたC言語APIも提供することにした。C言語のプロジェクトで使えることはもちろん、その他の言語で利用する際にもC言語APIをラップした方が楽なことがあるからだ。具体的に言えば、Go言語に組み込むためにC言語APIがあると便利だ。

Read more