RustのPython拡張モジュール









「絶対的な声明はすべての悪の根源です。

キーはバランスです。 答えはなく、質問だけです。」

????



記事の著者: zolkko

最適化


ソフトウェアのコンテキストでの最適化について話すとき、多くの場合、プログラマのパフォーマンスの最適化やソフトウェア自体の最適化を意味します。



YAGNIの原理に基づいて、Pythonはプログラマーがソフトウェアの実装に集中できるようにし、低レベルのもの(オブジェクトが割り当てられるメモリ領域、メモリの解放、規約の呼び出し)を処理する必要をなくします。



サイモン・ジョーンズは、Haskell に関する彼の講義の1つで反対の問題を指摘しました。 彼には、グラデーションで塗りつぶされた矢印のあるスライドがありました。 「No types」は最初に、「Haskell」は中央に、「Coq」は最後に書かれています。 Coqを指して、彼は次のように述べました。 そうですか! ここには博士号が必要です!」[1]。 冗談であるという事実にもかかわらず、Pythonのマントラは、この言語のプログラマーのお気に入りの機能の1つです。 私の経験では、これにより、完成品を少し速く生産することができます。











ソフトウェアの最適化に関しては、さまざまなソースがそれについて異なる方法で語っていますが、私にとっては3つのレベルに分けています。





ここで興味深い機能はこれです:最適化が実行されるレベルが高いほど、より効果的です。 通常はそのように。 一方、最適化のレベルが高いほど、早めに行う必要があります。プロジェクトの終わりに、アプリケーションアーキテクチャの再構築がより困難になることは明らかです。 また、ボトルネックが発生する場所を事前に特定することは困難です。また、要件が変更された場合はソフトウェアを変更することが難しくなるため、一般的に時期尚早な最適化を避けたいと思います。



ランタイム最適化


おそらくPythonコードの低レベル最適化のための最も論理的で正しい(面倒な点で)戦略は、PyPy、Pystonなどの特別なツールの使用です。 これは、よく使用されるCpythonコードがすでに最適であり、任意の行を追加しようとすると、パフォーマンスが低下する可能性が高いためです。 さらに、Pythonの動的型付けのため、従来の最適化手法を適用することはできません。



特に、この問題はPyston Talk 2015でKevin Modzelewskiによって指摘されました[2]。 彼によると、約10%のランタイムを期待できます。 JIT、トレースJIT、ヒューリスティック分析、Pystonなどのさまざまな手法を組み合わせることで、パフォーマンスを25%向上させることができます。

そして、ここに彼のレポートから取られた1つのベンチマークチャートがあります:







グラフは、ある時点でPyPyが通常のCpythonよりも38倍遅くなることを示しています。 結果は、そのようなツールを使用して、パフォーマンスを測定する必要があることを示唆しています。 そして、ソフトウェア実行の実際の条件に近い条件で、実際のデータでこれを行う必要があります。 そして、通訳バージョンの更新ごとにこのような演習を実行することをお勧めします。 ここでは、「最適化を行い、パフォーマンスの向上を確認するための測定を行わない場合、コードを読みにくくしたことだけが確実である」と引用できます[3]。



ソースコードの最適化


同様の問題は、言語レベルでの最適化中に、慣用的な量産コードを使用して特定できます。 説明のために、単語のリストとそれを大文字から単語のリストに変換する3つの関数が定義されている小さなプログラム(まったく慣用的ではない[4])の例を示します。



LST = list(map(''.join, product('abc', repeat=10))) def foo(): return map(str.upper, LST) def bar(): res = [] for i in LST: res.append(i.upper()) return res def baz(): return [i.upper() for i in LST]
      
      







その中で、3つの論理的に等価な関数は、意味的にもパフォーマンスにおいても異なります。 ただし、パフォーマンスセマンティクスは何も言いません。 いずれにせよ、経験の浅いPythonプログラマーの場合-while 1:pass vs while:True:pass-Python 3に切り替えるとデマになる危険性がある魔法。



CPythonモジュール


Pythonの低レベル最適化のもう1つのオプションは、拡張モジュールです。拡張モジュールにロジックの一部を削除すると、予測結果で良好なパフォーマンスを達成できる場合があります。



ツールキット


利用可能なPythonツールの多くは、CUDAのコード生成からnumpyまたはC ++との透過的な統合まで、さまざまな機能を提供します。 ただし、以下では、特別に選択された境界の例を使用して拡張モジュールを作成するコンテキストでのみ動作を検討します。



 def add_mul_two(a, b): acc = 0 i = 0 while i < 1000: acc += a + b i += 1 return acc
      
      







ご覧のとおり、CPythonは文字通りそれを実行します。



 12 SETUP_LOOP 40 (to 55) 15 LOAD_FAST 3 (i) 18 LOAD_CONST 2 (1000) 21 COMPARE_OP 0 (<) 24 POP_JUMP_IF_FALSE 54 27 LOAD_FAST 2 (acc) 30 LOAD_FAST 0 (a) 33 LOAD_FAST 1 (b) 36 BINARY_ADD 37 INPLACE_ADD 38 STORE_FAST 2 (acc) 41 LOAD_FAST 3 (i) 44 LOAD_CONST 3 (1) 47 INPLACE_ADD 48 STORE_FAST 3 (i) 51 JUMP_ABSOLUTE 15 54 POP_BLOCK
      
      







Cで最も単純な拡張モジュールを記述することにより、状況を修正できます。

これを行うには、最小モジュール初期化関数を決定します。



 // example.c void initexample(void) { Py_InitModule("example", NULL); }
      
      







この関数は、実際にimportステートメントを実行しているため、そのように呼び出されます...



 import example IMPORT_NAME 0 (example) STORE_FAST 0 (example)
      
      







...フルフィルメントにつながります...



 // ceval.c ... w = GETITEM(names, oparg); v = PyDict_GetItemString(f->f_builtins, "__import__"); ... x = PyEval_CallObject(v, w); ...
      
      







...組み込み関数builtin___import__(bltinmodule.c)、さらに呼び出しチェーンの下流:



 dl_funcptr _PyImport_GetDynLoadFunc(const char *fqname, const char *shortname, const char *pathname, FILE *fp) { char funcname[258]; PyOS_snprintf(funcname, sizeof(funcname), "init%.200s", shortname); return dl_loadmod(Py_GetProgramName(), pathname, funcname); }
      
      







いずれにせよ、一部のプラットフォームおよび特定の条件下では:CPythonは動的にロードされる拡張モジュールをサポートして構築され、モジュールはまだロードされていません。モジュールファイル名には特定のプラットフォーム固有の拡張があります。



次に、モジュールメソッドが決定され......



 static PyObject * add_mul_two(PyObject * self, PyObject * args); static PyMethodDef ExampleMethods[] = { {"add_mul_two", add_mul_two, METH_VARARGS, ""}, {NULL, NULL, 0, NULL} }; void initexample(void) { Py_InitModule("example", ExampleMethods); }
      
      







...およびその実装自体。 この場合、入力変数のタイプは正確にわかっているため、関数は次のように定義できます。



 PyObject * add_mul_two(PyObject * self, PyObject * args) { int a, b, acc = 0; if (!PyArg_ParseTuple(args, "ii", &a, &b)) { PyErr_SetNone(PyExc_ValueError); return NULL; } for (int i = 0; i < 1000; i++) acc += a + b; return Py_BuildValue("i", acc); }
      
      







出力は、Numbaを使用して取得できるものとほぼ同じバイナリコードになります...



 ___main__.add_mul_two$1.int32.int32: addl %r8d, %ecx imull $1000, %ecx, %eax movl %eax, (%rdi) xorl %eax, %eax retq
      
      







...しかし、2行だけを記述し、1つのプログラミング言語の制限を超えないようにします。



 @jit(int32(int32, int32), nopython=True)
      
      







このコードに加えて、numbaは以下を生成します



 add_mul_two.inspect_asm().values()[0].decode('string_escape')
      
      







...次の形式のラッパー関数:

 _wrapper.__main__.add_mul_two$1.int32.int32: ... movq %rdi, %r14 movabsq $_.const.add_mul_two, %r10 movabsq $_PyArg_UnpackTuple, %r11 ... movabsq $_PyNumber_Long, %r15 callq *%r15 movq %rax, %rbx xorl %r14d, %r14d testq %rbx, %rbx je LBB1_8 movabsq $_PyLong_AsLongLong, %rax …
      
      







そのタスクは、デコレータに記述されている署名に従って入力引数を解析し、成功した場合、コンパイルされたバージョンを実行することです。 この方法は非常に魅力的ですが、たとえば、別の関数でループの本体を取り出す場合は、デコレータでフレーム化するか、nopythonを無効にする必要もあります。



Cythonは次の挑戦者です。 これは、C関数の呼び出しとC型の定義をサポートするPythonのスーパーセットです。 したがって、最も単純な場合、そのadd_mul_two関数はCpythonに似ています。 ただし、広範な機能はそのようには提供されず、Cバージョンとは異なり、結果のファイルはCPython APIタイプのほぼ2,000行になります。



 __pyx_t_2 = PyNumber_Add(__pyx_v_a, __pyx_v_b); if (unlikely(!__pyx_t_2)) { __pyx_filename = __pyx_f[0]; __pyx_lineno = 14; __pyx_clineno = __LINE__; goto __pyx_L1_error; } __Pyx_GOTREF(__pyx_t_2); __pyx_t_3 = PyNumber_InPlaceAdd(__pyx_v_acc, __pyx_t_2); if (unlikely(!__pyx_t_3)) { __pyx_filename = __pyx_f[0]; __pyx_lineno = 14; __pyx_clineno = __LINE__; goto __pyx_L1_error; }
      
      







コードの量という点ではなく、特異性の点で状況を改善するには、たとえばCでの関数自体の実装を記述し、Cythonを使用してラッパーを決定します。



 int cadd_mul_two(int a, int b) { int32_t acc = 0; for (int i = 0; i < 1000; i++) acc += a + b; return acc; } cdef extern from "example_func.h": int cadd_mul_two(int, int) def add_two(a, b): return cadd_two(a, b) cythonize("sample.pyx", sources=[ 'example_func.c' ])
      
      







...ほぼ完璧なオプションが得られますが、この場合はすでにC、Cython、Pythonで記述する必要があります。



  __pyx_t_1 = __Pyx_PyInt_As_int32_t(__pyx_v_a); if (unlikely((__pyx_t_1 == (int32_t)-1) && PyErr_Occurred())) {__pyx_filename = __pyx_f[0]; __pyx_ __pyx_t_2 = __Pyx_PyInt_As_int32_t(__pyx_v_b); if (unlikely((__pyx_t_2 == (int32_t)-1) && PyErr_Occurred())) {__pyx_filename = __pyx_f[0]; __pyx_ __pyx_t_3 = __Pyx_PyInt_From_int32_t(cadd_two(__pyx_t_1, __pyx_t_2)); if (unlikely(!__pyx_t_3)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 8; _
      
      







さび


Rustでモジュールを作成するには、no_mangleでextern関数を宣言する必要があります...



 #[no_mangle] pub extern fn initexample() { unsafe { Py_InitModule4_64(&SAMPLE[0] as *const _, &METHODS[0] as *const _, 0 as *const _, 0, PYTHON_API_VERSION); }; }
      
      







...タイプを説明します。



 type PyCFunction = unsafe extern "C" fn (slf: *mut isize, args: *mut isize) -> *mut isize; #[repr(C)] struct PyMethodDef { pub ml_name: *const i8, pub ml_meth: Option<PyCFunction>, pub ml_flags: i32, pub ml_doc: *const i8, } unsafe impl Sync for PyMethodDef { }
      
      







Cと同様に、PyMethodを宣言する必要があります。



 lazy_static! { static ref METHODS: Vec = { vec![ PyMethodDef { ml_name: &ADD_MUL_TWO[0] as *const _, ml_meth: Some(add_mul_two), }, ... ] }; }
      
      







CPythonには多くのC API呼び出しがあるため、これも記述する必要があります。



 #[link(name="python2.7")] extern { fn Py_InitModule4_64(name: *const i8, methods: *const PyMethodDef, doc: *const i8, s: isize, apiver: usize) -> *mut isize; fn PyArg_ParseTuple(arg1: *mut isize, arg2: *const i8, ...) -> isize; fn Py_BuildValue(arg1: *const i8, ...) -> *mut isize; }
      
      







しかし、最終的には、まさにそのような美しい機能が得られます。



 #[allow(unused_variables)] unsafe extern "C" fn add_mul_two(slf: *mut isize, args: *mut isize) -> *mut isize { let mut a: i32 = 0; let mut b: i32 = 0; if PyArg_ParseTuple(args, &II_ARGS[0] as *const _, &a as *const i32, &b as *const i32) == 0 { return 0 as *mut _; } let mut acc: i32 = 0; for i in 0..1000 { acc += a + b; } Py_BuildValue(&I_ARGS[0] as *const _, acc) }
      
      







または、必要に応じて...

 let acc: i32 = (0..).take(1000) .map(|_| a + b) .fold(0, |acc, x| acc + x);
      
      







...この関数も2つのマシン命令にコンパイルされます。



 __ZN7add_mul_two20h391818698d43ab0ffcaE: ... callq 0x7a002 ## symbol stub for: _PyArg_ParseTuple testq %rax, %rax je 0x14e3 movl -0x8(%rbp), %eax addl -0x4(%rbp), %eax imull $0x3e8, %eax, %esi ## imm = 0x3E8 leaq _ref5540(%rip), %rdi ## literal pool for: "h" ...
      
      







このアプローチの欠点は次のとおりです。







しかし、幸いなことに、必要なすべてのCpythonAPIを既に説明しているだけでなく、それらに高レベルの抽象化を提供し、同時にPython 2.xおよび3.xをサポートする素晴らしいrust-cpythonプロジェクトがあります。 コードは次のようなものです。



 [package] name = "example" version = "0.1.0" [lib] name = "example" crate-type = ["dylib"] [dependencies] interpolate_idents = "0.0.9" [dependencies.cpython] version = "0.0.5" default-features = false features = ["python27-sys"]
      
      







 #![feature(slice_patterns)] #![feature(plugin)] #![plugin(interpolate_idents)] #[macro_use] extern crate cpython; use cpython::{PyObject, PyResult, Python, PyTuple, PyDict, ToPyObject, PythonObject}; fn add_two(py: Python, args: &PyTuple, _: Option<&PyDict>) -> PyResult<PyObject> { match args.as_slice() { [ref a_obj, ref b_obj] => { let a = a_obj.extract::<i32>(py).unwrap(); let b = b_obj.extract::<i32>(py).unwrap(); let mut acc:i32 = 0; for _ in 0..1000 { acc += a + b; } Ok(acc.to_py_object(py).into_object()) }, _ => Ok(py.None()) } }
      
      







 py_module_initializer!(example, |py, module| { try!(module.add(py, "add_two", py_fn!(add_two))); Ok(()) });
      
      







実際には、sclice_pattensおよびPyTuple.as_sliceに対してのみ、夜間のRustを使用します。

しかし、私の意見では、この状況でのRustは、強力で高レベルの抽象化、アルゴリズムとデータ構造の微調整機能、効果的で予測可能な最適化結果を備えたソリューションを提供します。 つまり、他のツールに代わる価値のある選択肢のように見えます。



この記事で使用されているサンプルコードを表示できます

こちら



書誌


1:サイモンペイトンジョーンズ、Haskellでのタイプの冒険-サイモンペイトンジョーンズ(講義2)、2014年、 youtu.be / brE_dyedGm0?t = 536

2:Kevin Modzelewski、2015/11/10 Pyston Meetup、2015、 www.youtube.com / watch?v = NdB9XoBg5zI

3:Martin Fowler、Yet Another OptimizationArticle、2002、 martinfowler.com / ieeeSoftware / yetOptimization.pdf

4:レイモンド・ヘッティンガー、コードを美しい、慣用的なPythonに変換、2013年、 www.youtube.com / watch?v = OSGv2VnC0go



All Articles