La historia de un experimento con Cython y el vector C ++

Uno tibio En una fría noche de invierno, quería calentarme en la oficina y comprobar la teoría de un colega de que el vector C ++ podría hacer frente a la tarea más rápido que la lista CPython.


En la compañía, estamos desarrollando productos basados ​​en Django, y resultó que era necesario procesar una gran variedad de diccionarios. Un colega sugirió que la implementación en C ++ sería mucho más rápida, pero no perdí la sensación de que Guido y la comunidad probablemente sean un poco más geniales que nosotros en C y que probablemente ya hayan decidido y evitado todos los obstáculos, implementando todo mucho más rápido.


Para probar la teoría, decidí escribir un pequeño archivo de prueba en el que decidí ejecutar en bucle la inserción de diccionarios 1M del mismo contenido en la matriz y el vector 100 veces seguidas.


Los resultados, aunque eran esperados, pero también repentinos.


Dio la casualidad de que estamos utilizando Cython activamente, por lo que, en general, los resultados serán diferentes en una implementación completa de CPython.


Pararse


  • Calcule Linux onegreyonewhite 4.18.14-calcule # 1 SMP PREEMPT Sáb 13 de octubre 21:03:27 UTC 2018 x86_64 Intel® Core (TM) i7-4770 CPU @ 3.40GHz GenuineIntel GNU / Linux
  • Python 2.7 y 3.6
  • Cython 0.28.3
  • gcc (Gentoo 7.3.0-r3 p1.4)

Guión


Por cierto, tuve que jugar aquí. Para obtener los números más realistas (es decir, no solo hacerlo súper optimizado, sino también para que podamos usarlo más tarde sin bailar con una pandereta), tuvimos que hacer todo en el guión principal y minimizar todo .h adicional.


El primer problema fue que el contenedor Cython para el vector no quiere funcionar así:


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

Por todo esto, obtuvieron un error de que es imposible lanzar un dict a PyObject. Por supuesto, estos son problemas de Cython, pero como lo usamos, necesitamos resolver este problema específico.
Tuve que hacer una pequeña muleta en forma de

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

Lo más sorprendente es que funcionó. Lo que más me asusta es que no entiendo completamente por qué y cuáles son las consecuencias.


Fuentes finales

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)) 

Intento 1


Realmente quiero poder recopilar * .whl para el proyecto y que todo terminó en casi cualquier sistema, por lo que el indicador de optimización se configuró primero en 0. Esto condujo a un resultado extraño:


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

Después de una pequeña reflexión, decidí que todavía usamos el indicador -O1, así que lo configuré de todos modos y lo obtuve:


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

De alguna manera me molesté un poco: sin embargo, la creencia en la profesionalidad de Guido y compañía me decepcionó. Pero luego, me di cuenta de que el script de alguna manera está comiendo sospechosamente la memoria y al final estaba consumiendo alrededor de 20 GB de RAM. El problema era este: en el script final, puede observar la función libre, después de pasar el ciclo. En esta iteración, todavía no estaba. Entonces pensé ...


¿Pero puedo desactivar gc?


Entre los intentos, hice gc.disable () y después de probar gc.enable () . Comienzo el ensamblaje y el script y obtengo:


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

En general, la diferencia no es grande, así que pensé que no tenía sentido pagar de más intente pervertir de alguna manera y simplemente use CPython, pero aún así recójalo con Cython.
Probablemente muchos tengan una pregunta: "¿Qué hay con la memoria?" Lo más sorprendente (no) es que nada. Ella creció al mismo ritmo y en la misma cantidad. Me vino a la mente un artículo , pero no quería entrar en las fuentes de Python. Sí, y esto significaba solo una cosa: el problema en la implementación del vector.


Final


Después de mucho tormento con la conversión de tipos, es decir, para que el vector acepte un puntero a un diccionario, se obtuvo el mismo script resultante y con gc activado obtuve una diferencia promedio de 2.6 veces (el vector es más rápido) y un rendimiento de memoria relativamente bueno.


De repente caí en la cuenta de que colecciono todo solo bajo Py2.7 y ni siquiera intenté hacer nada con 3.6.


Y aquí estaba realmente sorprendido (después de los resultados anteriores, la sorpresa fue lógica):


 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 

Con todo esto, gc aún funcionaba, la memoria no se engulló y era el mismo script. Al darme cuenta de que después de un poco más de un año, sería necesario decir adiós a 2.7, aún me preguntaba si había tanta diferencia entre ellos. Muy a menudo, escuché / leí / experimenté y Py3.6 fue más lento que Py2.7. Sin embargo, los chicos de los desarrolladores de Cython hicieron algo increíble y cambiaron la situación de raíz.


Resumen


Después de este experimento, decidimos no molestarnos mucho con el soporte para Python 2.7 y rehacer cualquier parte de las aplicaciones C ++, simplemente porque no vale la pena. Todo ya ha sido escrito antes que nosotros, solo podemos usarlo correctamente para resolver un problema específico.


UPD 24/12/2018:
Siguiendo el consejo de iCpu y después de los ataques laterales, se verifica que no entienda qué y cómo, intenté reescribir la parte de C ++ de la manera más conveniente para el desarrollo en el futuro, así como minimizar las abstracciones. Resultó aún peor:


El resultado de un pobre conocimiento de 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 

¿Alguna idea de lo que podría mejorarse en el copagador para que funcione más rápido?

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


All Articles