Cython和C ++向量的一项实验的故事

一个 温暖的 在一个寒冷的冬天晚上,我想在办公室里热身并测试一个同事的理论,即C ++向量可以比CPython列表更快地完成任务。


在该公司,我们正在开发基于Django的产品,并且碰巧需要处理大量词典。 一位同事建议,使用C ++的实现会快得多,但是我并没有感到Guido和社区可能比我们在C语言中更酷,并且可能已经决定并绕过了所有陷阱,从而更快地实现了一切。


为了验证该理论,我决定编写一个小的测试文件,在其中决定循环运行,将相同内容的1M字典插入数组,并向量连续插入100次。


结果,虽然他们是预期的,但也是突然的。


碰巧我们正在积极使用Cython,因此,通常,在完全CPython实现中,结果会有所不同。


展位


  • 计算Linux onegreyonewhite 4.18.14-计算#1 SMP PREEMPT星期六10月13日21:03:27 UTC 2018 x86_64Intel®Core(TM)i7-4770 CPU @ 3.40GHz纯正英特尔GNU / Linux
  • Python 2.7和3.6
  • Cython 0.28.3
  • gcc(Gentoo 7.3.0-r3 p1.4)

剧本


顺便说一下,我不得不在这里修补。 为了获得最真实的数字(即不仅要对其进行超级优化,而且还可以使我们稍后使用它而无需铃鼓跳舞),我们必须在主脚本中进行所有操作,并将所有其他.h最小化。


第一个问题是vector的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,并且几乎所有系统上都包含* .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源。 是的,这仅意味着一件事-实现向量的问题。


决赛


经过大量的类型转换折磨之后,即,向量接受了指向字典的指针,获得了相同的结果脚本,并且在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 

有什么想法可以在比较器中进行改进以使其更快地工作吗?

Source: https://habr.com/ru/post/zh-CN433852/


All Articles