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

Source: https://habr.com/ru/post/J279561/


All Articles