L'histoire d'une expérience avec le vecteur Cython et C ++

Un chaud Par une froide soirée d'hiver, je voulais me réchauffer au bureau et tester la théorie d'un collègue selon laquelle le vecteur C ++ pourrait faire la tâche plus rapidement que la liste CPython.


Dans l'entreprise, nous développons des produits basés sur Django, et il s'est avéré qu'il était nécessaire de traiter un large éventail de dictionnaires. Un collègue a suggéré que la mise en œuvre en C ++ serait beaucoup plus rapide, mais je n'ai pas perdu le sentiment que Guido et la communauté sont probablement un peu plus cool que nous en C et ont probablement déjà décidé et contourné tous les écueils, mettant tout en œuvre beaucoup plus rapidement.


Pour tester la théorie, j'ai décidé d'écrire un petit fichier de test dans lequel j'ai décidé d'exécuter en boucle l'insertion de 1M de dictionnaires du même contenu dans le tableau et le vecteur 100 fois de suite.


Les résultats, bien qu'ils soient attendus, mais aussi soudains.


Il se trouve que nous utilisons activement Cython, donc en général, les résultats différeront sur une implémentation entièrement CPython.


Stand


  • Calculer Linux onegreyonewhite 4.18.14-calculer # 1 SMP PREEMPT Sam Oct 13 21:03:27 UTC 2018 x86_64 Intel® Core (TM) i7-4770 CPU @ 3.40GHz GenuineIntel GNU / Linux
  • Python 2.7 et 3.6
  • Cython 0.28.3
  • gcc (Gentoo 7.3.0-r3 p1.4)

Script


Au fait, j'ai dû bricoler ici. Pour obtenir les nombres les plus réalistes (c'est-à-dire non seulement le rendre super-optimisé, mais aussi pour que nous puissions l'utiliser plus tard sans danser avec un tambourin), nous avons dû tout faire dans le script principal et minimiser tous les .h supplémentaires.


Le premier problème était que le wrapper Cython pour le vecteur ne voulait pas fonctionner comme ceci:


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

Pour tout cela, ils ont eu une erreur selon laquelle il est impossible de lancer un dict sur PyObject. Bien sûr, ce sont des problèmes Cython, mais puisque nous l'utilisons, nous devons résoudre ce problème spécifique.
J'ai dû faire une petite béquille en forme de

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

La chose la plus étonnante est que cela a fonctionné. Ce qui me fait le plus peur, c'est que je ne comprends pas bien pourquoi et quelles en sont les conséquences.


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

Tentative 1


Je veux vraiment pouvoir collecter * .whl pour le projet et que tout se soit terminé sur presque tous les systèmes, donc l'indicateur d'optimisation a d'abord été mis à 0. Cela a conduit à un résultat étrange:


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

Après une petite réflexion, j'ai décidé que nous utilisons toujours le drapeau -O1, alors je l'ai réglé tout de même et je l'ai:


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

D'une certaine manière, je me suis un peu énervé: néanmoins, la croyance dans le professionnalisme de Guido and Co. m'a déçu. Mais ensuite, j'ai remarqué que le script mangeait de façon suspecte de la mémoire et qu'à la fin, il consommait environ 20 Go de RAM. Le problème était le suivant: dans le script final, vous pouvez observer la fonction libre, après avoir passé la boucle. À cette itération, il ne l'était pas encore. Alors j'ai pensé ...


Mais puis-je désactiver gc?


Entre les tentatives, j'ai fait gc.disable () et après avoir essayé gc.enable () . Je démarre l'assemblage et le script et j'obtiens:


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

En général, la différence n'est pas grande, donc je pensais que ça ne servait à rien surpayer essayez de pervertir en quelque sorte et utilisez simplement CPython, mais collectez-le toujours avec Cython.
Beaucoup ont probablement une question: "Qu'y a-t-il avec la mémoire?" Le plus étonnant (non) est que rien. Elle a grandi au même rythme et en même quantité. Un article m'est venu à l'esprit, mais je ne voulais pas du tout entrer dans les sources Python. Oui, et cela ne signifiait qu'une chose - le problème dans la mise en œuvre du vecteur.


Finale


Après beaucoup de tourments avec la conversion de type, à savoir que le vecteur accepte un pointeur vers un dictionnaire, le même script résultant a été obtenu et avec gc activé, j'ai obtenu une différence moyenne de 2,6 fois (le vecteur est plus rapide) et des performances de mémoire relativement bonnes.


Soudain, il m'est apparu que je ne collectais tout que sous Py2.7 et je n'ai même pas essayé de faire quoi que ce soit avec 3.6.


Et là, j'ai été vraiment surpris (après les résultats précédents, la surprise était logique):


 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 

Avec tout cela, gc fonctionnait toujours, la mémoire ne se gobait pas, et c'était le même script. Réalisant qu'après un peu plus d'un an, il faudrait dire au revoir à 2.7, je me demandais quand même qu'il y avait une telle différence entre eux. Le plus souvent, j'ai entendu / lu / expérimenté et Py3.6 était plus lent que Py2.7. Cependant, les gars des développeurs de Cython ont fait quelque chose d'incroyable et ont changé la situation dans l'œuf.


Résumé


Après cette expérience, nous avons décidé de ne pas trop nous soucier de la prise en charge de Python 2.7 et de la refonte des parties d'applications C ++, simplement parce que cela n'en vaut pas la peine. Tout a déjà été écrit devant nous, nous ne pouvons l'utiliser correctement que pour résoudre un problème spécifique.


UPD 24/12/2018:
Sur les conseils d' iCpu et après les attaques sur le côté, il est vérifié de ne pas comprendre quoi et comment, j'ai essayé de réécrire la partie C ++ de la manière la plus pratique pour le développement à l'avenir, ainsi que de minimiser les abstractions. Cela s'est avéré encore pire:


Le résultat d'une mauvaise connaissance du 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 

Des idées qui pourraient être améliorées dans le coparator pour qu'il fonctionne plus rapidement?

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


All Articles