Die Geschichte eines Experiments mit Cython und C ++ Vektor

Eins warm An einem kalten Winterabend wollte ich mich im Büro aufwärmen und die Theorie eines Kollegen überprüfen, dass der C ++ - Vektor die Aufgabe schneller bewältigen kann als die CPython-Liste.


Im Unternehmen entwickeln wir Produkte, die auf Django basieren, und so kam es, dass eine große Anzahl von Wörterbüchern verarbeitet werden musste. Ein Kollege schlug vor, dass die Implementierung in C ++ viel schneller sein würde, aber ich habe nicht das Gefühl verloren, dass Guido und die Community wahrscheinlich etwas cooler sind als wir in C und wahrscheinlich bereits alle Fallstricke entschieden und umgangen haben und alles viel schneller implementiert haben.


Um die Theorie zu testen, habe ich beschlossen, eine kleine Testdatei zu schreiben, in der ich beschlossen habe, das Einfügen von 1 Million Wörterbüchern desselben Inhalts in das Array und den Vektor 100 Mal hintereinander in einer Schleife auszuführen.


Die Ergebnisse waren zwar zu erwarten, aber auch plötzlich.


Es ist einfach so passiert, dass wir Cython aktiv verwenden, sodass sich die Ergebnisse bei einer vollständigen CPython-Implementierung im Allgemeinen unterscheiden.


Stehen


  • Berechnen Sie Linux onegreyonewhite 4.18.14-berechne # 1 SMP PREEMPT Sa Okt 13 21:03:27 UTC 2018 x86_64 Intel® Core (TM) i7-4770 CPU bei 3,40 GHz GenuineIntel GNU / Linux
  • Python 2.7 und 3.6
  • Cython 0,28,3
  • gcc (Gentoo 7.3.0-r3 p1.4)

Skript


Übrigens musste ich hier basteln. Um die realistischsten Zahlen zu erhalten (d. H. Nicht nur superoptimiert zu machen, sondern auch, damit wir sie später verwenden können, ohne mit einem Tamburin zu tanzen), mussten wir alles im Hauptskript tun und alle zusätzlichen .h minimieren.


Das erste Problem war, dass der Cython-Wrapper für Vektor nicht so funktionieren möchte:


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

Trotz alledem haben sie den Fehler erhalten, dass es unmöglich ist, PyObject ein Diktat zu geben. Natürlich sind dies Cython-Probleme, aber da wir es verwenden, müssen wir dieses spezifische Problem lösen.
Ich musste eine kleine Krücke in Form von machen

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

Das Erstaunlichste ist, dass es funktioniert hat. Was mich am meisten erschreckt, ist, dass ich nicht ganz verstehe, warum und was die Konsequenzen haben.


Letzte Quellen

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

Versuch 1


Ich möchte wirklich in der Lage sein, * .whl für das Projekt zu sammeln und dass alles auf fast jedem System landete, daher wurde das Optimierungsflag zuerst auf 0 gesetzt. Dies führte zu einem seltsamen Ergebnis:


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

Nach einigem Nachdenken entschied ich, dass wir immer noch das Flag -O1 verwenden, also setzte ich es trotzdem und bekam es:


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

Irgendwie war ich ein bisschen verärgert: Trotzdem ließ mich der Glaube an die Professionalität von Guido und Co. im Stich. Aber dann bemerkte ich, dass das Skript irgendwie verdächtig Speicher verbraucht und am Ende ungefähr 20 GB RAM verbraucht. Das Problem war folgendes: Im endgültigen Skript können Sie die freie Funktion nach dem Übergeben der Schleife beobachten. Bei dieser Iteration war er noch nicht. Dann dachte ich ...


Aber kann ich gc deaktivieren?


Zwischen den Versuchen habe ich gc.disable () und nach dem Versuch von gc.enable () gemacht . Ich starte die Assembly und das Skript und erhalte:


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

Im Allgemeinen ist der Unterschied nicht groß, also dachte ich, dass es keinen Sinn macht Überzahlung versuche irgendwie zu pervertieren und benutze einfach CPython, sammle es aber trotzdem mit Cython.
Wahrscheinlich haben viele eine Frage: "Was ist da mit der Erinnerung?" Das Erstaunlichste (nein) ist, dass nichts. Sie wuchs mit der gleichen Geschwindigkeit und in der gleichen Menge. Ein Artikel kam mir in den Sinn, aber ich wollte überhaupt nicht auf die Python-Quellen eingehen. Ja, und dies bedeutete nur eines - das Problem bei der Implementierung des Vektors.


Finale


Nach vielen Qualen bei der Typkonvertierung, nämlich dass der Vektor einen Zeiger auf ein Wörterbuch nehmen würde, wurde das gleiche resultierende Skript erhalten, und bei eingeschaltetem gc erhielt ich einen durchschnittlich 2,6-fachen Unterschied (der Vektor ist schneller) und eine relativ gute Speicherleistung.


Plötzlich wurde mir klar, dass ich alles nur unter Py2.7 sammle und nicht einmal versuche, mit 3.6 etwas zu tun.


Und hier war ich wirklich überrascht (nach den vorherigen Ergebnissen war die Überraschung logisch):


 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 

Mit all dem funktionierte gc immer noch, der Speicher verschlang nicht und es war das gleiche Skript. Als ich merkte, dass es nach etwas mehr als einem Jahr notwendig sein würde, sich von 2.7 zu verabschieden, fragte ich mich immer noch, dass es einen solchen Unterschied zwischen ihnen gab. Am häufigsten hörte / las / experimentierte ich und Py3.6 war langsamer als Py2.7. Die Jungs von Cython-Entwicklern haben jedoch etwas Unglaubliches getan und die Situation im Keim verändert.


Zusammenfassung


Nach diesem Experiment haben wir beschlossen, uns nicht viel mit der Unterstützung von Python 2.7 zu beschäftigen und Teile von C ++ - Anwendungen neu zu erstellen, einfach weil es sich nicht lohnt. Alles wurde bereits vor uns geschrieben, wir können es nur richtig verwenden, um ein bestimmtes Problem zu lösen.


UPD 24.12.2008:
Auf Anraten von iCpu und nach Angriffen auf die Seite wird überprüft, um nicht zu verstehen, was und wie. Ich habe versucht, den C ++ - Teil auf die für die zukünftige Entwicklung bequemste Weise neu zu schreiben und Abstraktionen zu minimieren. Es stellte sich noch schlimmer heraus:


Das Ergebnis schlechter C ++ - Kenntnisse

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 

Irgendwelche Ideen, was im Coparator verbessert werden könnte, damit es schneller funktioniert?

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


All Articles