TadaoYamaokaの日記

山岡忠夫Homeで公開しているプログラムの開発ネタを中心に書いていきます。

C#からPythonへポインタ渡しする

以前にPython for .NETを使用することで、C#Pythonを同一プロセスで動かせることを紹介した。

C#からPythonへの呼び出し時に、引数と戻り値は、自動的にマーシャリングが行われる。
ここで、C#の型からPythonのネイティブな型に変換されるのは、スカラ型とリスト型に限られる。
残りは、C#のクラスをラッピングしたクラスになり、実態はC#側で処理されるオブジェクトになる。

課題

Unityで作成されたC#のプログラムから、PyTorchなどのPythonフレームワークを使用してモデルの学習を行いたい場合を想定する。
このような場合、C#で作成した入力特徴量をPythonに渡す必要がある。

入力特徴量をPyTorchのTensorに変換するには、リスト型か連続したメモリ領域である必要がある。
リスト型にするには、C#からList型で渡せば、自動でマーシャリングされるため、

torch.as_array(input)

とすれば、明示的な変換なしに変換できる。

しかし、入力特徴量は通常サイズが大きく、呼び出しのたびに、List型からPythonのlistへのマーシャリングが発生すると遅くなる。

できれば、連続したメモリ領域をそのまま渡したい。

なお、C#の配列(float[])を渡した場合、Single[]というラッピングされたクラスになるため、torch.as_arrayには入力できない。

ポインタ渡しする方法

Pythonには、ctypesというC言語とのインターフェースのためのライブラリが用意されている。
これを使用することで、ポインタを扱うことができる。

C#にも、オブジェクトのメモリ領域をポインタとして扱う機能がある。
ただし、ポインタとして扱っている間は、ガベージコレクションの対象にならないように、注意が必要である。
具体的には、以下の2つの方法がある。

GCHandle.Allocを使う

以下のコード例のように、GCHandle.AllocにPinnedを指定して、ガベージコレクションの対象にならないようにしたオブジェクトのアドレスを取得するためのハンドルを取得し、AddrOfPinnedObject()でポインタを取得する。
ポインタの使用が終わったらFree()を呼び出す。

using System.Runtime.InteropServices;


var array = new float[] { 1.0f, 2.0f, 3.0f, 4.0f };
var handler = GCHandle.Alloc(array, GCHandleType.Pinned);
var pointer = handler.AddrOfPinnedObject();

// ポインタを使用
python_module.function((Int64)pointer, array.Length);

handler.Free();
fixedを使用する

C#には、ポインタを扱うためのfixedという言語機能がある。

var array = new float[] { 1.0f, 2.0f, 3.0f, 4.0f };
fixed (float* pointer = &array[0])
{
    // ポインタを使用
    python_module.function((Int64)pointer, array.Length);
}                  

Free()が必要ないため、解放漏れの心配がなくなる。
ただし、unsafeを指定することが必須になる。
なお、GCHandle.Allocを使用する場合も、unsafeなことを行っているので、unsafeを指定しておく方が正しいと思われる。

Python側でポインタを扱う

次に、Python側で数値として渡されたポインタを扱う方法について述べる。

上で述べたctypesを使用することで、ポインタを扱うことができるが、PyTorchでは直接ポインタを入力することができたいため、一旦Numpyのオブジェクトにする。
Numpyには、ポインタで渡されたメモリ領域をそのままNumpyのオブジェクトとして使用する方法がある。

import numpy as np
import ctypes

def function(pointer, size):
    array = (ctypes.c_float * size).from_address(pointer)
    data = np.ctypeslib.as_array(array)

ctypesのfrom_address()で、引数で渡された数値のポインタをPOINTER型に変換し、np.ctypeslib.as_array()でNumpyのオブジェクトに変換している。

これを、PyTorchのTensorに変換することができる。

    tensor = torch.as_tensor(data)

ここまでで、C#から渡されたメモリ領域のコピーが発生してないことに注意して欲しい。
つまり、

    tensor[0] = 10

とすると、C#側のarrayの値も変更されることになる。

まとめ

Python for .NETを使用して、C#からPythonへポインタ渡しする方法について紹介した。
ポインタ渡しすることで、マーシャリングやメモリコピーが発生しないため、高速に処理できる。
ポインタを扱えるのは、同一プロセスでランタイムを動かせるPython for .NETのメリットの一つである。


アプリケーションによっては、特徴量の加工の処理が多くなるので、Pythonで処理すると遅くなる。
C#側で加工を行い、そのままTensorにできる状態にして、メモリ領域をポインタで渡せばPython側での加工をなくせるので高速に処理できる。
Tensorは次元の大きい疎ベクトルになることもあるので、ここでマーシャリングが入ってしまうとC#側で加工するメリットが失われてしまう。
その意味でも、ポインタをそのまま渡せるのがベストである。

ソース

github.com