はじめに
この記事ではD言語のmirというライブラリを使って,簡単にnumpy配列を処理する方法を提案します.D言語のmixinやUDAのおかげで3行追加するだけでD言語で書いた関数がpythonで使えるようになります。
元ネタ: RustでNumPyを拡張する
何故D?
Dは現代的なシステム言語で数値計算に向いている(と思う)
- C++/Javaによく似た文法
- C++的なメモリモデル・move semantics (no GCも可能)
- SFINAEを使わない小奇麗なTemplate
- 強力な型推論
- Native Thread
- LLVMによる最適化 (LDC)
- Dで書きたいから
D言語に興味があるPythonユーザの方は次の記事をおすすめです.
なお,D言語にはすでにNumpyのような多次元配列ライブラリMirが存在し,Pythonの効率が良い配列の共有方法Buffer Protocolの実装もありました.
やったこと
ただ実際には次のような追加のライブラリが必要だったので作りました
- Python側のNumpy配列オブジェクト等からBuffer Protocolへの変換ライブラリ
- 良い感じにmir.ndslice.Sliceを引数にとる関数に
@pybuffer
とアノテーションするとBuffer Protocol用のラッパー関数を生成するライブラリ
GitHub: https://github.com/ShigekiKarita/mir-pybuffer
インストール
Python側にはpipでインストールします
$ pip install pybuffer
D言語のプロジェクトファイルdub.jsonにはパッケージの追加と動的ライブラリ生成のオプションが必要です.例えば次のように書きます.
{
"name": "your-dub-lib",
"authors": ["you"],
"dependencies": {
"mir-pybuffer": "~>0.1.7"
},
"targetType": "dynamicLibrary",
"description": "A minimal D application.",
"copyright": "Copyright © 2018, you",
"license": "BSL-1.0"
}
D言語側の使い方
まずD言語側の関数の書き方です.この例ではdoubleの行列matの一列目にベクトルvecを足して,vecをa倍する関数です.これに追加で3行加えるとPythonから使えるようになります.
-
import pybuffer : pybuffer, MixinPyBufferWrappers;
でmir-pybufferパッケージを読み込む -
@pybuffer
としてBuffer Protocolに対応した関数を生成するようにマークする -
mixin MixinPyBufferWrappers;
で自動的にSliceをPy_buffer構造体に変換するラッパー関数を生成する
import mir.ndslice : Slice, Contiguous;
import pybuffer : pybuffer, MixinPyBufferWrappers; // 1
@pybuffer // 2
void func1(Slice!(Contiguous, [2LU], double*) mat, Slice!(Contiguous, [1LU], double*) vec, double a) {
mat[0][] += vec;
vec[] *= a;
}
mixin MixinPyBufferWrappers;
まだ開発中のライブラリなので注意点が2つあります.
-
@pybuffer
はPy_buffer -> Slice変換時のエラーコードを返す関数を作るので,元関数の戻り値はvoidでないとコンパイルエラーになります. -
import pybuffer : pybuffer, MixinPyBufferWrappers;
でなくimport pybuffer;
と書くとコードが生成されません.原因は調査中です. -
@pybuffer
された関数の中で,GCを使うと固まります.原因は調査中です.ただしmirはほぼ全てのAPIがGCなしで動くのであまり問題にはならないと思います.
使えるようになりました(v0.1.7) thanks @lempiji
Python側の使い方
こちらはとくに注意点はなく,普通にctypesを使ってdubによって生成されたD言語の動的ライブラリを読み込むだけです.
import ctypes
import numpy
import pybuffer
x = numpy.array([[0, 1, 2], [3, 4, 5]]).astype(numpy.float64)
y = numpy.array([0, 1, 2]).astype(numpy.float64)
lib = pybuffer.CDLL("./libyour-dub-lib.so")
err = lib.func1(x, y, ctypes.c_double(3.0))
assert err == 0
assert x == numpy.array([[0, 2, 4], [3, 4, 5]])
assert y == numpy.array([0, 3, 6])
ctypes.CDLLと違ってpybuffer.CDLLクラスは自動変換された関数を優先して呼び出し,numpy配列などは自動的にPy_buffer構造体に変換されます.
開発用ですが,実際に生成されたラッパー関数は lib.print_generated()
で表示することができます.挙動がおかしいときは確認してGitHubまでご連絡ください.
おわりに
いかがでしょうか、D言語にはGCや強力なメタプログラミング機能があるので,他の言語よりも遥かに簡単にnumpyの多次元配列が処理できると思います。今ならライブラリ本体も100行くらいで、とても簡単なのでPR等もお待ちしています。
Appendix
全体のサンプルコードはこちらを見てください
D言語ではなく,C言語でPy_buffer構造体を読み書きする例もあります.