2018年2月19日

C++ から任意の Python 関数を実行する

Calling any Python function from C++
Abstract
  There is a few methods to run python functions on C++ like boost::python or using of Python.h. For example, using boost.python needs to convert C++ types like std::vector<T> frequently used as a standard implementation to Python types like numpy enable to give them as a function arguments. While this method will provide high execution efficiency, users need to prepare or convert types like std::vector<T> to Python types. For these reasons, sstd::c2py has been developed in order to call any Python functions with built-in, std::vector<T>, sstd::mat_c<T> or sstd::mat_r<T> (T is limitted by built-in types.) types from 2 lines of C++ codes.

  C++ から Python 関数を呼び出すには,boost::python や Python.h ヘッダからの API 呼び出しが知られている.例えば,boost::python を利用する場合は,C++ 側で Python 側へそのまま受け渡しができる numpy 型等を用意した上で python 関数の呼び出しを行う ([8] より推察).この手法は,高い実行効率を得られる反面,標準実装として C++ で頻繁に用いられる std::vector<T> 型をそのまま利用しようとした際に,ユーザ側で変換しておく必要がある ([9] より推察) など,実装に必要な手間も大きい.そこで,C++ の built-in 型 および std::vector<T>, sstd::mat_c<T>, sstd::mat_r<T> (ただし,T は built-in 型に限る.) と同等の numpy 型を引数に取る任意の Python 関数を C++ から 2 行で実行するライブラリを開発した.
Introduction
  C++ と比較して Python は,計算速度で劣るものの,資産管理が優れている.そのため,Windows か Linux かを問わず,また多少コンピュータに不慣れであっても,Anaconda をインストールするだけで,画像の編集や,簡単なテキスト操作に必要な機能の殆どを揃えることができる ※.データ解析においては,言語を問わずグラフ描画の需要は存在するが,現状では C++ に十分な機能を持った「手軽な」グラフ描画ライブラリを確認できなかった.一方で,Python の matplotlib はグラフ描画ライブラリとして非常に有名であり,C++ ユーザであっても,言語間の橋渡しをして利用することがある.

※ 当然 Linux 上であれば,C++ でも必要なパッケージを $ apt install XXX である程度簡単にインストールできる.また,Python についても $ pip 等で環境を構築する方法もある.
Previous methods
  matplotlib は,名の知れた Python のグラフ描画ライブラリである.matplitlib を C++ から利用するには,
  1) データを CSV へ書き出し,Python に読み込ませる
  2) Python のコマンドライン引数に値を流し込む [1]
  3) パイプを用いその都度データを流し込む [2]
  4) Boost.Python を利用する
  5) matplotlib の C++ 用ラッパを利用する [3][4]
等の方法がある.1),2) については,記述が複雑で本投稿の要請を満たさない.3) については,リアルタイム描画が主であり,いくらか複雑性が増すため,本投稿の趣旨とは異なる※.4),5) については,開発当時,筆者環境で実行できなかったため考慮から外す.なお,5) は,matplotlib のラッパとなるため,どこまで実装されているか等の確認が必要である.

※ 本投稿は,リアルタイム描画について扱うものではないが,数秒おきに同じファイル名でグラフをプロットし,書き込み禁止を指定しない自動更新する画像ビュワーを利用すれば,原理上は半リアルタイムで描画される.
※ 2018年12月15日追記. 4) について,未検証だが [C++からPythonを叩きつつ、boost.numpyを使ってC++とPython間でndarrayをやりとりする - verilog書く人 - 2018年12月15日閲覧][8] は綺麗に実装されており参考となる.
Purpose
  本投稿では,単なる matplotlib のラッパではなく,任意の Python 関数のラッパを作成することで,同一の枠組みの中で,より多くの応用を与える汎用的なプログラムの作成を目標とする.この際,なるべく簡潔なインターフェイスを設計する.
Method
  C++ から Python に値を受け渡す仕組みについて説明する.手法はいくつか存在するが,ここでは,C++ から受け渡す値をバイナリファイルとして一時ディレクトリに書き込み,Python インタプリタを起動した後,Python 側からバイナリファイルを読みこませる.このとき C++ とバイナリ互換性のある numpy を Python 側のデフォルト型とすることで,バイナリ値を変換することなく,受け渡しが可能となる.ただし,組み込み型へ変換できるようにオプションを用意した (Sample4 参照).ファイルへの書き出しは,全く古典的な手法であるが,より多くの環境で動作させる必要があったため,プロセス間通信は利用しなかった.
  また,任意の関数を実行するためには,単に値を受け渡すだけでなく,関数の引数情報も一緒に受け渡す必要があった.そのため,sstd::c2py クラスをインスタンス化する時に,型情報を文字列として受け渡す手法が考えられた.文字列としての受け渡しは,実行時オーバーヘッドを伴うが,C++ コンパイラのさらなる発展に期待しつつ,本投稿ではインターフェースの簡潔さが優先された.
  本ライブラリでは,まず,インターフェースが設計された.次に,どこまでの環境で実行可能とするか,例えば,Cygwin のような仮想環境で動作するか,また,Python.h へのパスが通っている必要があるかが検討された.実行速度に関しては,グラフプロットが実用的な時間で行えることを目標とした.

  より詳細な説明へ移る.sstd::c2py クラスのコンストラクタは,Python 関数の呼び出しに必要な値を取得するため,次のように設計された.

  Following is the design of c2py interface.
sstd::c2py<A type of return value> Function name("Tempolary directory",                               // Argument 1
                                                 "A name of calling .py file (without extension)",    // Argument 2
                                                 "A function name calling from .py file",             // Argument 3
                                                 "Arguments types specification of Python function"); // Argument 4

  c2py インターフェースの設計.
sstd::c2py<戻り値の型> 関数名("一時ディレクトリ",                               // 第 1 引数
                              "呼び出し先の .py ファイル (ただし拡張子を除く)", // 第 2 引数
                              ".py ファイル中から呼び出す関数名",               // 第 3 引数
                              "Python 関数の型指定");                           // 第 4 引数

引数には,値の受け渡しに使用される一時ディレクトリ,実行する Python 関数を含む .py ファイルへのパス (ただし,拡張子を除く),.py ファイル中にある実行対象の Python 関数名,実行する Python 関数の引数情報,をそれぞれ指定する.このとき,sstd::c2py クラスのインスタンス化した名前を,Python 関数の名前に揃えておくと分り易い.ここまでで C++ 側では,Python 関数の実行に必要な値を得ることができる.
  次に,sstd::c2py クラスのインスタンス化時の名前を,関数のように振る舞わせることを考える.これには,初期化されたクラスに対して () 演算子を定義すればよい.すると,クラスから () 演算子を呼び出すことで,関数のように扱うことができる.この際,() 演算子の引数を可変長引数として扱う必要がある.C 言語における可変長引数は,引数の個数を与える必要があるが,ここでは,クラス初期化時に第 4 引数を解析することで,引数の数を取得する.この際,型情報は対応する予約済みの番号に置き換える.C++ は,テンプレートからも可変長引数を扱えるが,可変長テンプレートでは引数の const,非 const の識別ができず,また,より複雑な型指定ができないため,本投稿には適さない.
  () 演算子から引数を受け取ったあと,データ転送用の一時ディレクトリを作成する.一時ディレクトリの構成は,"./第 1 引数/[unixtime]_[microsec]/~" となる ※1.この際,ディレクトリ "第 1 引数" は,削除しない.これは,同一の一時ディレクトリを共有していた場合,別の c2py クラスが利用しているデータを誤って削除することを回避するためである.それ以降の "[unixtime]_[microsec]" については,() 演算子の終了時に削除する.
  一時ディレクトリが作成されると,転送用の型情報が argList.bin へ,各引数の値が argXXXX.bin (XXXX は引数番号.0000 は戻り値) へ書き込まれる.その後,c2py.py が,コマンドライン引数に一時ディレクトリ名 ("./第 1 引数/[unixtime]_[microsec]"),呼び出し先の .py ファイル名,呼び出し関数名,を与えた状態で呼び出される.
  c2py.py は,一時ディレクトリを読みこんだ後,対象の .py ファイルを動的に import する.最後に,関数名へ,引数指定文字列から復元した引数を動的に与え,Python 関数を呼び出す.
  対象の Python 関数が実行されると,c2py.py は,非 const のポインタ型について,一時ファイルへ値の書き戻しを行う.この際,変更が反映されるかどうかは,Python 側の仕様による.具体的には,リストのように値の受け渡しがアドレス行われる場合は,関数内での値の変更が,呼び出し元の Python コードまで波及し,書き戻した後の値は変化する.引数の値の変化の有無にかかわらず,最終的に c2py.py/c2py.cpp は,全ての非 const のポインタ型について, () 演算子の引数へ書き戻しを行う.(ソースコード上では,この操作を write back と呼称している.)

  実装上の問題点として,テンプレートを多用した実装が行われたため,ヘッダサイズの増加に伴い,コンパイル時間の増加が発生した.このため,ヘッダを可能な限り関数化することで,分割コンパイルによるキャッシュが利用されるようにし,コンパイル時間を削減している.

※1. これは,コードの実行タイミングがミリ秒オーダーで一致した場合,正常に動作しないことを意味する.シングルスレッドで問題になるとは考えにくいが,マルチスレッドでは問題となる可能性がある.個別の一時ディレクトリを作成するか,コンストラクタの呼び出し時刻を追加する等の対策が考えられる.簡単には,第 1 引数を "sstd::ssprintf("./tmpDir/%s_%d",__func__,__LINE__).c_str()" とする方法がある.第 1 引数に指定されたディレクトリは再帰的に作成されるため,深さを憂慮する必要はない.
Implementation
  実装するにあたり,各型ごとの処理を動的に変更する必要がある.C++ 側ですべての型に対応させることは,困難であるが,基本的な型について,全て対応させることで,多くの Python 関数を呼び出すことができる.このため,本投稿では,表 1. に示す型について,実装を行った.
  表 1. について,C++ 側の型については,それぞれ実体・ポインタ・ポインタ配列を受け渡せる.それぞれの特徴として,まず,実体渡しの場合は,c2py の内部で変数をコピーするため,ポインタ渡しと比較してコストが大きくなる.次に,ポインタ渡しの場合は,const を指定しない場合,c2py が,python 側での引数の上書きの有無に関わらず,引数に値を書き戻す.したがって,const char* 等の一時変数として宣言された引数を与える場合,const の指定を行わないと,アクセス違反を引き起こすため注意が必要である.最後に,ポインタ配列の受け渡しは,表の一番左の列の型のみ可能で,len によって,配列長を指定する必要がある.配列長が指定されない場合は,len = 1; として処理される.Python 側の型について,何も指定しない場合は,numpy 側が選択され,変換記号 ~ を指定することで,built-in 型に変換される.c2py 上の処理は,C++ とのバイナリ互換性の問題から,基本的に numpy 型で行われるため,built-in 型への変換は,オーバーヘッドとなる.

Table 1. Correspondence between types implemented in c2py and Python
表 1. c2py で実装されている型と Python 型との対応
Types of C++ side Types of Python side
  Entity / Pointer
/ Pointer array
Entity / Pointer
Entity / Pointer
Entity / Pointer
Entity / Pointer
   bool std::vector<bool> sstd::mat_c<bool> sstd::mat_r<bool> numpy / built-in
   char std::vector<char> sstd::mat_c<char> sstd::mat_r<char> built-in
   uchar std::vector<uchar> sstd::mat_c<uchar> sstd::mat_r<uchar> built-in
   int8 std::vector<int8> sstd::mat_c<int8> sstd::mat_r<int8> numpy / built-in
   int16 std::vector<int16> sstd::mat_c<int16> sstd::mat_r<int16> numpy / built-in
   int32 std::vector<int32> sstd::mat_c<int32> sstd::mat_r<int32> numpy / built-in
   int64 std::vector<int64> sstd::mat_c<int64> sstd::mat_r<int64> numpy / built-in
   uint8 std::vector<uint8> sstd::mat_c<uint8> sstd::mat_r<uint8> numpy / built-in
   uint16 std::vector<uint16> sstd::mat_c<uint16> sstd::mat_r<uint16> numpy / built-in
   uint32 std::vector<uint32> sstd::mat_c<uint32> sstd::mat_r<uint32> numpy / built-in
   uint64 std::vector<uint64> sstd::mat_c<uint64> sstd::mat_r<uint64> numpy / built-in
   float std::vector<float> sstd::mat_c<float> sstd::mat_r<float> numpy / built-in
   double std::vector<double> sstd::mat_c<double> sstd::mat_r<double> numpy / built-in
   std::string std::vector<std::string> sstd::mat_c<std::string> sstd::mat_r<std::string> built-in

Table 2. Type names giving to the 4th argument of c2py.
表 2. c2py の第 4 引数に与える型名.
Types of C++ side
          Entity / Pointer
/ Pointer array
Entity / Pointer
Entity / Pointer
Entity / Pointer
   bool vec<bool> mat_c<bool> mat_r<bool>
   char vec<char> mat_c<char> mat_r<char>
   uchar vec<uchar> mat_c<uchar> mat_r<uchar>
   int8 vec<int8> mat_c<int8> mat_r<int8>
   int16 vec<int16> mat_c<int16> mat_r<int16>
   int32 vec<int32> mat_c<int32> mat_r<int32>
   int64 vec<int64> mat_c<int64> mat_r<int64>
   uint8 vec<uint8> mat_c<uint8> mat_r<uint8>
   uint16 vec<uint16> mat_c<uint16> mat_r<uint16>
   uint32 vec<uint32> mat_c<uint32> mat_r<uint32>
   uint64 vec<uint64> mat_c<uint64> mat_r<uint64>
   float vec<float> mat_c<float> mat_r<float>
   double vec<double> mat_c<double> mat_r<double>
   str / string vec<str> / vec<string> mat_c<str> / mat_c<string> mat_r<str> / mat_r<string>
Operating environment
  動作環境:
GCC 4.4.7, 5.4.0, 6.4.0
Anaconda (Python 2.7 or 3.6 version)
Source code
  プログラム全体をブログに添付するには長いため,主な実装を下記の URL に示す.

Implementation (実装)
github.com/~/sstd/src/c2py.hpp
github.com/~/sstd/src/c2py.cpp

github.com/~/sstd/src/c2py.py

Test codes (テストコード)
github.com/~/main.cpp: void TEST_c2py();

ドキュメント
github.io/sstdref/src/c2py.html
Implementation period
  This program had been developed for 59 days from December 20, 2017 to February 17, 2018, consuming off time of my work.
  本プログラムは,2017 年 12 月 20 日から 2018 年 02 月 17 日にかけての 59 日間,本職の合間を縫って開発された.
Results
  ここでは,実装された機能を,サンプルコードとして示す.

Sample1
  ./pyFunctions.py
def plus_a_b(a, b): return a+b
  ./main.cpp
#include <sstd/sstd.hpp>

int main(){
    // int is treated as a int32. (uint is treated as a uint32.)

    sstd::c2py<int> plus_a_b("./tmpDir", "pyFunctions", "plus_a_b", "int, int, const int*");
    int a=1, b=2;
    int c=plus_a_b(a, &b);

    sstd::printn(c);
    return 0;
}
  Execution result
c = 3

Sample2
  ./pyFunctions.py
def plus_vecA_vecB(vecA, vecB): return vecA+vecB
  ./main.cpp
#include <sstd/sstd.hpp>

int main(){
    sstd::c2py<std::vector<int>> plus_vecA_vecB("./tmpDir", "pyFunctions", "plus_vecA_vecB", "vec<int>, const int*, len, const vec<int>*");
    int arrA[]={1,2,3};
    std::vector<int> vecB={4,5,6};
    std::vector<int> vecC=plus_vecA_vecB(arrA, 3, &vecB);

    sstd::printn(vecC);
    return 0;
}
  Execution result
vecC[3] = [ 5 7 9 ]

Sample3
  Writing back self multiplied value.
  自己乗算値を書き戻す.

  ./pyFunctions.py
def selfMult(a, vecB, vecC):
    a[0]=a[0]*a[0]
    for i in range(len(vecB)): vecB[i]=vecB[i]*vecB[i]
    for i in range(len(vecC)): vecC[i]=vecC[i]*vecC[i]
  ./main.cpp
#include <sstd/sstd.hpp>

int main(){
    sstd::c2py<void> selfMult("./tmpDir", "pyFunctions", "selfMult", "void, int*, int*, len, vec<int>*");
    int a=2;
    int arrB[]={3,4,5};
    std::vector<int> vecC={6,7,8};
    selfMult(&a, arrB, 3, &vecC);

    sstd::printn(a);
    printf("arrB[3] = [ "); for(uint i=0; i<3; i++){ printf("%d ", arrB[i]); } printf("]\n");
    sstd::printn(vecC);
    return 0;
}
  Execution result
a = 4
arrB[3] = [ 9 16 25 ]
vecC[3] = [ 36 49 64 ]

Sample4
  Conversion types in Python side. Symbols on the right side of "|" (which is a separator symbol between C++ and Python) mean the symbols have effect on the Python side. On the right side of "|" enable to take "*" or "~" and these order have no meaning. (There is no difference between "|*~" and "|~*", so it will work same.)
    *: A symbol have a meaning to convert input value on Python side to a pseudo pointer type (self inclusion list).
    ~: A symbol have a meaning to convert input value on Python side to a built-in type (instead of numpy type).

  Python 側で型変換を行う場合.セパレータ記号 "|" の左右は,それぞれ,C++ 側と Python 側を表している.分割記号 "|" の右側の型は "*" または "~" を取ることができ,これは Python 側における変換記号である.このとき,変換記号 "*","~" の順序は意味をなさない.(したがって, "|*~" と "|~*" の間に差はなく,同じように動作する.)
    *: Python 側の入力値を擬似ポインタ型 (自己包含リスト) へ変換する.
    ~: Python 側の入力値を(numpy 型の代わりに)組み込み型に変換する.

  ./pyFunctions.py
def checkTypes(Numpy, builtIn, pNumpy, pBuiltIn):
    print(type(Numpy), Numpy)
    print(type(builtIn), builtIn)
    print(type(pNumpy), pNumpy)
    print(type(pBuiltIn), pBuiltIn)
  ./main.cpp
#include <sstd/sstd.hpp>

int main(){
    sstd::c2py<void> checkTypes("./tmpDir", "pyFunctions", "checkTypes", "void, int, int|~, int|*, int|*~");
    checkTypes(0, 0, 0, 0);
    return 0;
}
  Execution result
(<type 'numpy.ndarray'>, array([0]))
(<type 'list'>, [0])
(<type 'list'>, [array([0])])
(<type 'list'>, [[0]])

Sample5
  Writing back with changing the length of std::vector<T>. (In order to get value from function, sending address is needed.)
  配列長の変化を含む std::vector<T> の書き戻し.(関数から値を受け取るため,アドレスを受け渡している)

  ./pyFunctions.py
import numpy as np
def changeLen(pVec1, vec2):
    pVec1[0]=np.append(pVec1[0], 4) # numpy    # numpy is not able to add values without changing address of variables. so we need to treat as a pointer like objects (self inclusion list).
    vec2.append(4)                  # built-in
  ./main.cpp
#include <sstd/sstd.hpp>

int main(){
    sstd::c2py<void> changeLen("./tmpDir", "pyFunctions", "changeLen", "void, vec<int>*|*, vec<int>*|~");
    std::vector<int> vec1={1,2,3}, vec2={1,2,3};
    changeLen(&vec1, &vec2);

    sstd::printn(vec1);
    sstd::printn(vec2);
    return 0;
}
  Execution result
vec1[4] = [ 1 2 3 4 ]
vec2[4] = [ 1 2 3 4 ]

Sample6
  Receiving multiple return values from python side. (※ "ret" which is a symbol of return value, must be continuous in arg 4. Interrupted ret occurs error.)
  Python 側から複数の戻り値を受け取る.(※ 戻り値記号 "ret" は,第 4 引数中で連続している必要がある.不連続な ret はエラーを引き起こす.)

  ./pyFunctions.py
def multiRet(): return (9, 9, [1,2,3], [4,5,6])
  ./main.cpp
#include <sstd/sstd.hpp>

int main(){
    sstd::c2py<int> multiRet("./tmpDir", "pyFunctions", "multiRet", "int, ret int*, ret int*, len, ret vec<int>*");
    int ret0=0;
    int ret1=0;
    int ret2[]={0,0,0};
    std::vector<int> ret3;
    ret0 = multiRet(&ret1, &ret2, 3, &ret3);

    sstd::printn(ret0);
    sstd::printn(ret1);
    printf("ret2[3] = [ "); for(uint i=0; i<3; i++){ printf("%d ", ret2[i]); } printf("]\n");
    sstd::printn(ret3);
    return 0;
}
  Execution result
ret0 = 9
ret1 = 9
ret2[3] = [ 1 2 3 ]
ret3[3] = [ 4 5 6 ]

Benchmarks
  ここでは,c2py による Python 関数の呼び出しにおけるオーバーヘッドを計測する.

実行環境:
・Intel Core i5-5200U CPU @ 2.20GHz
・DDR3 SDRAM 4.0GB
・HGST HTS545050A7E680
・Microsoft Windows 8.1 x64
・Cygwin, GCC 5.4.0
・Anaconda (Python 2.7 version)

Benchmark1
  ./pyFunctions.py
def emptyFunc(): return;
  ./main.cpp
#include <sstd/sstd.hpp>

int main(){
    time_m timem; sstd::measureTime_start(timem);

    sstd::c2py<void> emptyFunc("./tmpDir", "pyFunctions", "emptyFunc", "void");
    for(uint i=0; i<1000; i++){ emptyFunc(); }

    sstd::measureTime_stop(timem); sstd::pauseIfWin32(); return 0;
}
  Execution result
--------------------------------
 Execution time:   199. 535 sec
--------------------------------

  したがって,初期化後の関数呼び出し 1 回あたりのオーバーヘッドは,1000 分の 1 となり,約 200 ms となる.

Benchmark2
  ./pyFunctions.py
def emptyFunc(): return;
  ./main.cpp
#include <sstd/sstd.hpp>

int main(){
    time_m timem; sstd::measureTime_start(timem);

    for(uint i=0; i<1000; i++){
        sstd::c2py<void> emptyFunc("./tmpDir", "pyFunctions", "emptyFunc", "void");
        emptyFunc();
    }

    sstd::measureTime_stop(timem); sstd::pauseIfWin32(); return 0;
}
  Execution result
--------------------------------
 Execution time:   200. 207 sec
--------------------------------

  したがって,初期化を含めた関数呼び出し 1 回あたりのオーバーヘッドは,1000 分の 1 となり,約 200 ms となる.

  Benchmark1,Benchmark2 の結果から,c2py.hpp に実装されたの初期化処理自体には殆ど時間がかかっておらず,関数呼び出しにおいて多くのオーバーヘッドが発生していると分かる. 詳細は省くが,この関数呼び出しにおけるオーバーヘッドは,Python インタプリタの起動および,import に消費されていると考えられ, ファイルを通したデータの転送自体は,少なくとも転送データ量が少ない場合については,ボトルネックではないと考えられる.(これは,ディスクドライブのキャッシュが動作し,実際の処理がメモリ上で完結している可能性などが考えられる.)
  Python インタプリタ起動のオーバーヘッドは,c2py.hpp に実装されたコンストラクタ呼び出し時に,インタプリタを起動してスタンバイさせるなどの方法が考えられる. また,より現実的なコードとして,pyFunctions.py 内で多くのモジュールを import すると,読み込みに時間がかかる. 現状では,pyFunctions.py は,python 関数呼び出し時に,毎回動的に import されているため,python のラッパ関数 (c2py.py) 呼び出し時に,静的に import できるよう, import 文を C++ 側から動的に書き加えた c2py.py を,ファイルへ書き出してから実行するなどの工夫が必要と考えられる.
Discussion
  既存の手法と比較して,非常に簡潔なインターフェースでとなった.
  実行速度としては,200 ms/call 程度となった.例えば,画像 10,000 枚の画像をプロットする場合,単純計算で 34 分以上の時間が必要となる.(現実的には,プロットの処理時間等でより多くの時間を必要とする).この実行速度は速いとは言えないが,仮に 1 万枚のプロットが必要であっても,テスト段階のプロットでは,間を間引いて数百枚のプロットに収まると考えられる.したがって,実際には数分の待ち時間で収まると考えられ,ある程度実用的な実行速度であると考えられる.
Summary
  本成果物は,ほぼ任意の Python 関数を,C++ から 2 行で呼び出すことを可能とした.
  関数 1 回の呼び出し時間は,200 ms 程度となり,ある程度実用的な実行速度に収まった.
Future work
  優先順位の高い項目から順に下記に示す.

「Anaconda2の環境ではQt関連のエラーが解消できず,Anaconda3を入れ直すことで動くようになった.[7]」との記述を発見したので,Anaconda 3 系で一度 Python.h 回りを確認する.上手くいけば, https://github.com/lava/matplotlib-cpp/blob/master/matplotlibcpp.h#L311 等を参考に,numpy C-API を利用したデータの転送を検討する. この際,2 つ以上の戻り値を取得できるか,また write back を受け取れるか,等を確認する必要がある.場合によっては c2py の仕様を削ることも一つの選択肢ではある.
オーバーヘッドを減らすため,一時ファイルによる値の転送を,一つのファイルにまとめる.これは,プロセス間通信を行う際の通信コードの簡略化にも寄与する.ただし,先に Python 側で十分にバイナリが扱えることを確認する必要がある.
プロセス間通信を名前付きパイプで実装し,一時ファイルを不要にする.(プロセス間通信には,ソケット通信・共有メモリ (mmap)・パイプ・名前付きパイプ,があるが,ソケット通信はポート番号の衝突等を考慮する必要があり,共有メモリは動的なメモリサイズの変更が困難.殆どファイルと同じように扱えることから,この中では,名前付きパイプが一番筋がよいと思われる.ただし,バッファサイズが小さく詰まる可能性がある.いずれも,GCC と MSVC++ で扱いが異なるため,まずはこの違いを埋めるコードを書く.)
現状で,少なくとも 200 ms 掛かっている呼び出し時間を削減する.この実装には,Python インタプリタをスタンバイさせる必要があり,上記の項目の実装が必要になると考えられる.実装は,単純にコンストラクタ呼び出しと同時にインタプリタを起動し,ディストラクタが呼び出されるまで使い回せばよい.
アセンブラがテーブルジャンプになるよう if else を switch に変更する.ただし,もっとも最適には,ポインタ・コンスタントの有無,および,std::vector・sstd::mat_c・sstd::mat_r の全ての型に対して,個別に番号を振る必要がり,コードの行数増加と共に,可読性は落ちる.また,スコープを {} で制限するように注意.他のオーバーヘッドと比較し,大勢に影響なさそうなので,優先順位は低い.
構造体 (Python では class) の転送への対応.簡単には,構造体を引数に文字列で与え,構造を解析したのち,型情報とデータ転送し,Python 側で動的に復元する.文字列としてクラスを生成し,exec で実行すればよい.C++ 側について,構造体に可変長型およびポインタ型が含まれていなければ,そのままバイナリとしてコピーすればよい.構造体に可変長型が含まれている場合は,構造解析した結果から,構造体の何バイト目に可変長型があるかを計算し,そのアドレスをポインタとして取得すれば,あとは自在に可変長型へアクセスできるはずである.[5] 等で指摘さているように,データ構造アライメントの問題が発生するため,適切に注意深く実装を行う必要がある.簡単には,まず,コンパイラの違い等の影響でアライメントの変わる心配のない型に制限した実装を行うことが考えられる.正しく制限した実装を行い,適切なエラーメッセージさえ提供すれば,それなりに実用に耐えるはずである.(offsetof での位置情報取得はマクロがコンパイル時の前処理として実行されるため,コードを動的に書き換えてからコンパイルを行うなどの荒業が必要と推察される.) もしかすると,boost::serialization の仕様が参考になるかもしれない.
実行対象となる Python 関数を,C++ 側のクラス初期化時に,文字列として渡せるようにすることで,C++ コード内に Python 関数を埋め込めるようにする.ただし,Python の実行時エラーが示す行数が実際の行数とずれるため,マクロを仕込み行番号を把握した後,改行コードを余分に追加することで調整する.
Python から任意の C++ 関数を呼び出し可能とする.対象の C++ 関数へ個別のラップ処理をユーザに記述させることを避けるためには,Python から C++ を動的にコンパイルすればよい.この際の C++ コードは,エントリーポイントとなる事前に作成した通信用の C++ コードに,Python 側で事前に指定した .hpp ヘッダを書き加え,ヘッダに記述されている実行対象の C++ 関数の名前を,通信用コードの末尾に書き加え,ファイルに保存したのち,GCC 等でコンパイルを行う.コンパイルは,実行ファイルの時刻が,.cpp ファイルよりも古い場合にのみ行えばよい.つまり,コンパイルは一度行えば 2 回目以降は生成済みバイナリを使用すればよく,実行時間に大きな影響は与えない.Python と C++ 間の通信は,同様に一時ファイルかプロセス間通信を行えばよい.MSVC++ については,コマンドラインからのコンパイルはできたはずなので,不可能ではない.これについては,Boost.Python でも実装されている機能のため,実装する必要があるかどうか,もう一度検討を行う.
operator() 内部での分岐のテンプレート化.現状の C++ コンパイラは引数が const の関数と 非 const の関数を多重定義しようとすると,multiple definitions とエラーを返し,識別できない.このため,可変長テンプレートでは同等の機能を提供できない.まずは,C++ コンパイラを const と非 const を識別できるように改良する必要がある.
クラスの初期化処理の高速化.これは,通常の最適化以外にも,C++ の constexper という機能の利用が考えられる.しかし,現状の constexper は,for 文や while 文を利用することができない.現状の constexper がどのように実装されているかは調査が必要である.必要な機能としては,引数が定数かつ constexper の指定されている関数ないしクラスを,一度コンパイルした後に実行し,その結果をテキストでソースコードに埋め込み,もう一度コンパイルを行えば,事足りる.ソースコードへの埋め込みが現状の C++ で不可能であれば,まずはこの機能から拡張する必要がある.いずれにしても,ユーザに for 文や while 文が使えないといった制約を課すべきではなく,このように機能が制限されるべきでもない.そして,今回のように,クラスや関数の初期化手順がコンパイル時に完全に決定できる場合の最適化は,本来コンパイラが担うことが最も望ましい.したがって,最適化すべきは,このコードではなくコンパイラである.しかしながら,C++ は,他にも,例えば,行列計算を考える上でアダマール積等の要素演算を定義する演算子 .*  ./  .\  .^  .' ※1 が定義されていない,ないし,オーバーロードできないといった制約や,スクリプト言語のように A[1:3] のような要素アクセスを表現できないなど,大きの制約を抱えているが,現状では解決されるという話は聞いていない.したがって,C++ コンパイラの前処理として,C++ を拡張した言語を実装するというのは,一つの選択肢だと考えられる.(※2019年01月02日追記: C++ の新しい標準は日々変わっているので,注視する必要がある.ただし,ここでの問題を解決してくれるかは不明である.)

  ただし,もしも matplotlib にしか興味がないのであれば,訳の分らないラッパよりも
C++ ネイティブで,UI に一貫性があり,リアルタイム描画にも耐え得る高速な描画ライブラリの開発
を行うべきだろう.

※1. 演算子 .*  ./  .\  .^  .' は,MATLAB で要素演算用の演算子として定義されている [6].しかしながら,C++ ではこれらを代替できる演算子は存在しない.特に,直接メンバポインタ .* については,C++ で定義されているにも関わらず,オーバーロードできない演算子の筆頭である.どうしてもオーバーロードできないのであれば,逆順とした *. をアダマール積等に割り当てても実用上何ら問題はないのではあるが,とにかく,C++ にこうした演算子をサポートしようという気配は現状把握していない.
References


Appendix
Application sample 1
  As one of the most convenient application, sstd::c2py enable to call matplotlib which is a famous graph plot library in python from C++. In the code below, generate sin wave on C++ and write graph by matplotlib in Python.

  最も便利な応用の 1 つとして,sstd::c2py では,Python で有名なグラフプロットライブラリである matplotlib を C++ から呼び出すことができる.下記のサンプルコードでは,C++ 側で生成した sin 波を,Python ライブラリである matplotlib で描画している.

  ./pyFunctions.py
import matplotlib as mpl        # "QXcbConnection: Could not connect to display" への対策
mpl.use('Agg')                  # "QXcbConnection: Could not connect to display" への対策
import matplotlib.pyplot as plt # "QXcbConnection: Could not connect to display" への対策
import matplotlib.ticker as tick

def vec2graph(writeName, vecX, vecY):
    plt.clf()
    fig = plt.figure(figsize=(9, 3)) # アスペクト比の設定
    ax1 = fig.add_subplot(111)
    ax1.plot(vecX, vecY, color='k', linewidth=0.5)
    
    title  = "An example of Plotting a figure of sin wave data generated on C++,\n"
    title += "using matplotlib which is a famous graph plotting library of python. \n"
    title += "\"sstd::c2py()\" convertes a type of std::vector<double> on C++ to  \n"
    title += "numpy array type on Python, and calling a Python function from      \n"
    title += "only 2 lines of C++ code.                                                                    "
    ax1.set_title(title)
    
    ax1.grid(which='minor', linewidth=0.5, linestyle=':',  color='gainsboro')
    ax1.grid(which='major', linewidth=0.5, linestyle='-',  color='silver'    )
    
    ax1.tick_params(pad=5, which='major', direction='in', bottom=True, top=True, left=True, right=True, length=4) # 軸の余白 # which: major tick と minor tick に対して変更を適用 # tick を内側方向に # tick を bottom, top, left, right に付加 # tick width # tick length
    ax1.tick_params(pad=5, which='minor', direction='in', bottom=True, top=True, left=True, right=True, length=2) # 軸の余白 # which: major tick と minor tick に対して変更を適用 # tick を内側方向に # tick を bottom, top, left, right に付加 # tick width # tick length
    
    ax1.set_xlabel("Time [sec]\nFig 1.  0.1 Hz sin wave sampled by 10 Hz, 0-60 sec.")
    ax1.set_xlim(0-1, 60+1)
    ax1.xaxis.set_major_locator(tick.MultipleLocator(5))
    ax1.xaxis.set_minor_locator(tick.MultipleLocator(1))
    
    ax1.set_ylabel("Amplitude")
    ax1.set_ylim(-1.1, 1.1)
    ax1.yaxis.set_major_locator(tick.MultipleLocator(0.5))
    ax1.yaxis.set_minor_locator(tick.MultipleLocator(0.1))
    
    plt.savefig(writeName, bbox_inches="tight")

  ./main.cpp
#include <sstd/sstd.hpp>

int main(){
    double freq2generate = 0.1; // 0.1 Hz sin wave
    double freq2sample = 10;    // 10 Hz sampling
    uint len=60*10 + 1;         // 60 sec
    std::vector<double> vecY = sstd::sinWave(freq2generate, freq2sample, len);
    std::vector<double> vecX(len); for(uint i=0; i<vecX.size(); i++){ vecX[i]=(double)i*(1/freq2sample); }
    
    sstd::c2py<void> vec2graph("./tmpDir", "pyFunctions", "vec2graph", "void, const char*, vec<double>*, vec<double>*");
    vec2graph("./sin.png", &vecX, &vecY);

    return 0;
}

  Execution result
Application sample 2
Added on May 27, 2019.
  An example of the additional implementation of vvec<T>. Currently, only vvec<double> is available.

  追加実装された vvec<T> の使用例.現状では,vvec<double> のみ利用可能.

  ./pyFunctions.py
def vvec2graph(writeName, vLabel, vvecX, vvecY):
    plt.clf()
    fig = plt.figure(figsize=(8.5, 3)) # アスペクト比の設定
    ax1 = fig.add_subplot(111)
    #cmap = plt.get_cmap("tab10")
    vColor=['black', 'blue', 'red']
    vLineStyle = ['solid', 'solid', 'solid'] # solid, dashed, dashdot, dotted
    for i in range(len(vvecX)):
        #ax1.plot(vvecX[i], vvecY[i], linewidth=0.5, color=cmap(i), linestyle=vLineStyle[i], label=vLabel[i])
        ax1.plot(vvecX[i], vvecY[i], linewidth=0.5, color=vColor[i], linestyle=vLineStyle[i], label=vLabel[i])
    ax1.legend(loc='upper right')
    
    ax1.grid(which='minor', linewidth=0.5, linestyle=':',  color='gainsboro')
    ax1.grid(which='major', linewidth=0.5, linestyle='-',  color='silver'    )
    
    ax1.tick_params(pad=5, which='major', direction='in', bottom=True, top=True, left=True, right=True, length=4) # 軸の余白 # which: major tick と minor tick に対して変更を適用 # tick を内側方向に # tick を bottom, top, left, right に付加 # tick width # tick length
    ax1.tick_params(pad=5, which='minor', direction='in', bottom=True, top=True, left=True, right=True, length=2) # 軸の余白 # which: major tick と minor tick に対して変更を適用 # tick を内側方向に # tick を bottom, top, left, right に付加 # tick width # tick length
    
    ax1.set_xlabel("Time [sec]\nFig 2.  0.1 Hz sin, cos and -cos wave sampled by 10 Hz, 0-60 sec.")
    ax1.set_xlim(0-1, 60+1)
    ax1.xaxis.set_major_locator(tick.MultipleLocator(5))
    ax1.xaxis.set_minor_locator(tick.MultipleLocator(1))
    
    ax1.set_ylabel("Amplitude")
    ax1.set_ylim(-1.1, 1.1)
    ax1.yaxis.set_major_locator(tick.MultipleLocator(0.5))
    ax1.yaxis.set_minor_locator(tick.MultipleLocator(0.1))
    
    plt.legend(loc='best')
    plt.savefig(writeName, bbox_inches="tight") # , dpi=100

  ./main.cpp
#include <sstd/sstd.hpp>

int main(){
    double freq_generate = 0.1; // 0.1 Hz sin wave
    double freq_sample = 10;    // 10 Hz sampling
    uint len=60*10 + 1;         // 60 sec
    std::vector<double> sinY = sstd::sinWave(freq_generate, freq_sample, len);
    std::vector<double> sinX(len); for(uint i=0; i<sinX.size(); i++){ sinX[i]=(double)i*(1/freq_sample); }
    
    std::vector<double> cosY = sstd::cosWave(freq_generate, freq_sample, len);
    std::vector<double> cosX(len); for(uint i=0; i<cosX.size(); i++){ cosX[i]=(double)i*(1/freq_sample); }
    
    std::vector<std::string> vLabel={"sin", "cos", "-cos"};
    std::vector<std::vector<double>> vvecX={sinX, cosX,    cosX};
    std::vector<std::vector<double>> vvecY={sinY, cosY, -1*cosY};
    
    sstd::c2py<void> vvec2graph(tmpDir, fileName, "vvec2graph", "void, const char*, const vec<str>*, const vvec<double>*, const vvec<double>*");
    vvec2graph("./sin_cos.png", &vLabel, &vvecX, &vvecY);
    
    return 0;
}

  Execution result
Application sample 3
  In the code below, reading png image from Python, editing on C++ and writing to png file by Python again.

  Python 関数から png ファイルを読み込み,C++ で色を編集した後,再度 Python 関数で png ファイルへ書き出すサンプルコードを示す.

  ./pyFunctions.py
import numpy as np
from PIL import Image

def imgPath2mat_rRGB(path):
    imgRaw = Image.open(path)
    imgRGB = imgRaw.split()
    imgR = imgRGB[0]
    imgG = imgRGB[1]
    imgB = imgRGB[2]
    return (imgR, imgG, imgB)

def mat_rRGB2img(path, imgR, imgG, imgB):
    imgCombined = np.dstack((np.dstack((imgR, imgG)), imgB))
    imgPIL      = Image.fromarray(imgCombined)
    imgPIL.save(path)

  ./main.cpp
#include <sstd/sstd.hpp>

int main(){
    sstd::c2py<void> imgPath2mat_rRGB("./tmpDir", "pyFunctions", "imgPath2mat_rRGB", "void, ret mat_r<uint8>*, ret mat_r<uint8>*, ret mat_r<uint8>*, const char*");
    sstd::mat_r<uint8> imgR, imgG, imgB;
    imgPath2mat_rRGB(&imgR, &imgG, &imgB, "./sample.png");

    for(uint p=0; p<imgG.rows(); p++){
        for(uint q=0; q<imgG.cols(); q++){
            imgG(p, q) = sstd::round2even(0.5*((double)imgG(p, q)));
        }
    }
    
    sstd::c2py<void> mat_rRGB2img("./tmpDir", "pyFunctions", "mat_rRGB2img", "void, const char*, mat_r<uint8>*, mat_r<uint8>*, mat_r<uint8>*");
    mat_rRGB2img("./sample_reCombined.png", &imgR, &imgG, &imgB);

    return 0;
}

  Execution result
                                Input image (sample.png)                       Output image (sample_reCombined.png)

All of sample codes are in the below URL.
Usage
  Please use latest sstd.


2018.02.19 公開
2019.01.04 訂正

0 件のコメント:

コメントを投稿