CythonとC ++ベクトルを使用した1つの実験のストーリー

あたたかい 寒い冬の夜、私はオフィスでウォームアップし、C ++ベクターがCPythonリストよりも速くタスクを実行できるという同僚の理論をチェックしたかった。







同社では、Djangoをベースにした製品を開発しているため、1つの大きな辞書を処理する必要がありました。 同僚は、C ++での実装ははるかに高速になると示唆しましたが、GuidoとコミュニティはおそらくCでの私たちよりも少しクールで、おそらくすべての落とし穴を既に決定してバイパスし、すべてをはるかに高速に実装しているという感覚を残しませんでした。







理論をテストするために、同じコンテンツの1Mディクショナリを配列とベクトルに100回連続で挿入するループで実行することにした小さなテストファイルを作成することにしました。







結果は、予想されていましたが、突然でした。







たまたま、Cythonを積極的に使用しているため、一般的に、完全なCPython実装では結果が異なります。







スタンド





スクリプト



ところで、私はここでいじくり回さなければなりませんでした。 最も実数を取得するには(つまり、単に最適化するだけでなく、タンバリンと踊らなくても後で使用できるようにするため)、メインスクリプトですべてを実行し、追加の.hをすべて最小化する必要がありました。







最初の問題は、ベクターのCythonラッパーが次のように動作したくないことです。







#    ctypedef vector[object] dict_vec #     (   vector.push_back(dict())) ctypedef vector[PyObject*] dict_vec #   ,   ( ,    object   PyObject.) ctypedef vector[PyObject] dict_vec
      
      





これらすべてのために、PyObjectに辞書をキャストすることは不可能であるというエラーを受け取りました。 もちろん、これらはCythonの問題ですが、私たちはそれを使用しているため、この特定の問題を解決する必要があります。

の形で小さな松葉杖を作らなければなりませんでした





 #include "Python.h" static PyObject * convert_to_pyobject(PyObject *obj) { return obj; }
      
      





最も驚くべきことは、それが働いたことです。 私が一番怖いのは、その結果に伴う理由とその原因を完全に理解していないことです。







最終ソース

cython_experiments.h







 #include "Python.h" static PyObject * convert_to_pyobject(PyObject *obj) { return obj; }
      
      





cython_experiments.pyx







 # -*- coding: utf-8 -*- # distutils: language = c++ # distutils: include=['./'] # distutils: extra_compile_args=["-O1"] from __future__ import unicode_literals import time from libc.stdlib cimport free from cpython.dict cimport PyDict_New, PyDict_SetItemString from cpython.ref cimport PyObject from libcpp.string cimport string from libcpp.vector cimport vector cdef extern from "cython_experiments.h": PyObject* convert_to_pyobject(object obj) ctypedef vector[PyObject*] dict_vec range_attempts = 10 ** 6 # Insert time cdef test_list(): t_start = time.time() data_list = list() for i from 0 <= i < range_attempts: data_list.append(dict( name = 'test_{}'.format(i), test_data = i, test_data2 = str(i), test_data3 = range(10), )) del data_list return time.time() - t_start cdef test_vector(): t_start = time.time() cdef dict_vec *data_list data_list = new dict_vec() data_list.resize(range_attempts) for i from 0 <= i < range_attempts: data = PyDict_New() PyDict_SetItemString(data, 'name', 'test_{}'.format(i)) PyDict_SetItemString(data, 'test_data', i) PyDict_SetItemString(data, 'test_data2', str(i)) PyDict_SetItemString(data, 'test_data3', range(10)) data_list.push_back(convert_to_pyobject(data)) free(data_list) return time.time() - t_start # Get statistic times = dict(list=[], vector=[]) attempts = 100 for i from 0 <= i < attempts: times['list'].append(test_list()) times['vector'].append(test_vector()) print(''' Attempt: {} List time: {} Vector time: {} '''.format(i, times['list'][-1], times['vector'][-1])) avg_list = sum(times['list']) / attempts avg_vector = sum(times['vector']) / attempts print(''' Statistics: attempts: {} list avg time: {} vector avg time: {} '''.format(attempts, avg_list, avg_vector))
      
      





試行1



プロジェクトの* .whlを収集し、ほとんどすべてのシステムでそれがすべて終了するようにしたいので、最適化フラグが最初に0に設定されました。これは奇妙な結果につながりました。







 Python 2.7 Statistics: attempts: 100 list avg time: 2.61709237576 vector avg time: 2.92562381506
      
      





少し熟考した後、私はまだ-O1フラグを使用することに決めたので、すべて同じように設定して取得しました。







 Python 2.7 Statistics: attempts: 100 list avg time: 2.49274396896 vector avg time: 0.922211170197
      
      





どういうわけか私は少し動揺しました:それにもかかわらず、Guido and Co.のプロフェッショナリズムに対する信念は私を失望させました。 しかし、その後、スクリプトがなんとなく疑い深くメモリを消費し、最終的には約20GBのRAMを消費していることに気付きました。 問題はこれでした。最終スクリプトでは、ループを通過した後、free関数を観察できます。 この繰り返しでは、彼はまだでした。 その後、私は思った...







しかし、GCをオフにしますか?



試行の間に、 gc.disable()を作成し、 gc.enable()を試行した後。 アセンブリとスクリプトを開始して、以下を取得します。







 Python 2.7 Statistics: attempts: 100 list avg time: 1.00309731514 vector avg time: 0.941153049469
      
      





一般に、差は大きくないので、意味がないと思いました 過払い なんらかの方法で変質してCPythonを使用するだけで、Cythonで収集します。

おそらく多くの人が質問をしている:「そして記憶はどうなのか?」 最も驚くべき(いいえ)は何もないことです。 彼女は同じ速度で同じ量で成長しました。 記事が思い浮かびましたが、私はPythonのソースコードには一切入りたくありませんでした。 はい、これはたった1つのことを意味しました-ベクトルの実装の問題です。







ファイナル



型変換で多くの苦労の後、つまり、ベクターが辞書へのポインターを取るように、同じ結果のスクリプトが取得され、gcをオンにすると、平均で2.6倍の差があり(ベクターは高速です)、メモリで比較的良い仕事をしました。







突然、Py2.7の下でのみすべてを収集し、3.6で何もしようとさえしなかったことに気付きました。







そして、ここで私は本当に驚きました(前の結果の後、驚きは論理的でした):







 Python 3.6 Statistics: attempts: 100 list avg time: 0.8771139788627624 vector avg time: 1.075702157020569 Python 2.7 Statistics: attempts: 100 list avg time: 2.61709237576 vector avg time: 0.92562381506
      
      





このすべてで、gcはまだ機能し、メモリはゴブアップせず、同じスクリプトでした。 一年以上たってから2.7に別れを告げる必要があることに気付いた私は、それらの間にそのような違いがあるのか​​と疑問に思っていました。 ほとんどの場合、聞いた/読んだ/実験したが、Py3.6はPy2.7よりも遅かった。 しかし、Cythonの開発者たちは信じられないようなことをし、状況を変えました。







まとめ



この実験の後、Python 2.7のサポートとC ++アプリケーションの一部の再作成にあまり煩わされないことにしました。 すべてがすでに私たちの前に書かれているので、特定の問題を解決するためにのみ正しく使用できます。







UPD 12/24/2018:

iCpuのアドバイスとサイドへの攻撃の後、何をどのように理解していないかをチェックし、将来の開発に最も便利な方法でC ++部分を書き直し、抽象化を最小限に抑えようとしました。 さらに悪化しました。







C ++の不十分な知識の結果

cython_experiments.h







 #include "Python.h" #include <vector> #include <algorithm> #ifndef PyString_AsString #define PyString_AsString PyUnicode_AsUTF8 #define PyString_FromString PyUnicode_FromString #endif typedef struct { char* name; bool reverse; } sortFiled; class cmpclass { public: cmpclass(std::vector<char*> fields) { for (std::vector<char*>::iterator it = fields.begin() ; it < fields.end(); it++){ bool is_reverse = false; char* name; if (it[0] == "-"){ is_reverse = true; for(int i=1; i<strlen(*it); ++i) name[i] = *it[i]; } else { name = *it; } sortFiled field = {name, is_reverse}; this->fields_to_cmp.push_back(field); } } ~cmpclass() { this->fields_to_cmp.clear(); this->fields_to_cmp.shrink_to_fit(); } bool operator() (PyObject* left, PyObject* right) { // bool result = false; for (std::vector<sortFiled>::iterator it = this->fields_to_cmp.begin() ; it < this->fields_to_cmp.end(); it++){ // PyObject* str_name = PyString_FromString(it->name); PyObject* right_value = PyDict_GetItem(right, str_name); PyObject* left_value = PyDict_GetItem(left, str_name); if(!it->reverse){ result = left_value < right_value; } else { result = (left_value > right_value); } PyObject_Free(str_name); if(!result) return false; } return true; } private: std::vector<sortFiled> fields_to_cmp; }; void vector_multikeysort(std::vector<PyObject *> items, PyObject* columns, bool reverse) { std::vector<char *> _columns; for (int i=0; i<PyList_GET_SIZE(columns); ++i) { PyObject* item = PyList_GetItem(columns, i); char* item_str = PyString_AsString(item); _columns.push_back(item_str); } cmpclass cmp_obj(_columns); std::sort(items.begin(), items.end(), cmp_obj); if(reverse) std::reverse(items.begin(), items.end()); } std::vector<PyObject *> _test_vector(PyObject* store_data_list, PyObject* columns, bool reverse = false) { int range_attempts = PyList_GET_SIZE(store_data_list); std::vector<PyObject *> data_list; for (int i=0; i<range_attempts; ++i) { data_list.push_back(PyList_GetItem(store_data_list, i)); } vector_multikeysort(data_list, columns, reverse); return data_list; }
      
      





cython_experiments.pyx







 # -*- coding: utf-8 -*- # distutils: language = c++ # distutils: include=['./'] # distutils: extra_compile_args=["-O2", "-ftree-vectorize"] from __future__ import unicode_literals import time from libc.stdlib cimport free from cpython.dict cimport PyDict_New, PyDict_SetItemString from cpython.ref cimport PyObject from libcpp.string cimport string from libcpp.vector cimport vector import gc cdef extern from "cython_experiments.h": vector[PyObject*] _test_vector(object store_data_list, object columns, int reverse) range_attempts = 10 ** 6 store_data_list = list() for i from 0 <= i < range_attempts: store_data_list.append(dict( name = 'test_{}'.format(i), test_data = i, test_data2 = str(i), test_data3 = range(10), )) # Insert time def multikeysort(items, columns, reverse=False): items = list(items) columns = list(columns) columns.reverse() for column in columns: # pylint: disable=cell-var-from-loop is_reverse = column.startswith('-') if is_reverse: column = column[1:] items.sort(key=lambda row: row[column], reverse=is_reverse) if reverse: items.reverse() return items cdef test_list(): t_start = time.time() data_list = list() for i in store_data_list: data_list.append(i) data_list = multikeysort(data_list, ('name', '-test_data'), True) for i in data_list: i del data_list return time.time() - t_start cdef test_vector(): t_start = time.time() data_list = _test_vector(store_data_list, ['name', '-test_data'], 1) for i in data_list: i return time.time() - t_start # Get statistic times = dict(list=[], vector=[]) attempts = 10 gc.disable() for i from 0 <= i < attempts: times['list'].append(test_list()) times['vector'].append(test_vector()) gc.collect() print(''' Attempt: {} List time: {} Vector time: {} '''.format(i, times['list'][-1], times['vector'][-1])) del store_data_list avg_list = sum(times['list']) / attempts avg_vector = sum(times['vector']) / attempts print(''' Statistics: attempts: {} list avg time: {} vector avg time: {} '''.format(attempts, avg_list, avg_vector))
      
      





 Python 3.6 Statistics: attempts: 10 list avg time: 0.2640914678573608 vector avg time: 2.5774293661117555
      
      





より高速に動作するようにコパレーターで改善できるアイデアはありますか?








All Articles