A história de um experimento com o vetor Cython e C ++

Um quente Em uma noite fria de inverno, eu queria me aquecer no escritório e testar a teoria de um colega de que o vetor C ++ poderia executar a tarefa mais rapidamente do que a lista do CPython.


Na empresa, estamos desenvolvendo produtos baseados no Django, e aconteceu que era necessário processar uma grande variedade de dicionários. Um colega sugeriu que a implementação em C ++ seria muito mais rápida, mas não perdi a sensação de que Guido e a comunidade provavelmente são um pouco mais legais do que nós em C e provavelmente já decidiram e contornaram todas as armadilhas, implementando tudo muito mais rapidamente.


Para testar a teoria, decidi escrever um pequeno arquivo de teste no qual decidi executar em loop a inserção de 1 milhão de dicionários do mesmo conteúdo na matriz e vetor 100 vezes seguidas.


Os resultados, embora fossem esperados, mas também repentinos.


Aconteceu que estamos usando ativamente o Cython, portanto, em geral, os resultados diferem em uma implementação totalmente em CPython.


Suporte


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

Script


A propósito, eu tive que mexer aqui. Para obter os números mais realistas (ou seja, não apenas torná-lo super otimizado, mas também para que possamos usá-lo mais tarde sem dançar com um pandeiro), tivemos que fazer tudo no script principal e minimizar todos os .h adicionais.


O primeiro problema foi que o wrapper Cython para vetor não deseja funcionar assim:


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

Por tudo isso, eles obtiveram um erro de que é impossível lançar um ditado ao PyObject. Obviamente, esses são problemas do Cython, mas como o usamos, precisamos resolver esse problema específico.
Eu tive que fazer uma muleta pequena na forma de

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

O mais surpreendente é que funcionou. O que mais me assusta é que não entendo completamente o porquê e quais são as consequências.


Fontes finais

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

Tentativa 1


Eu realmente quero poder coletar * .whl para o projeto e que tudo acabou em quase qualquer sistema, para que o sinalizador de otimização tenha sido definido como 0. Isso levou a um resultado estranho:


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

Depois de um pouco de reflexão, decidi que ainda usaríamos o sinalizador -O1, então defini tudo da mesma forma e obtive:


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

De alguma forma, fiquei um pouco chateado: no entanto, a crença no profissionalismo de Guido e companhia me decepcionou. Mas, então, notei que o script, de alguma forma, suspeita está comendo memória e, no final, estava consumindo cerca de 20 GB de RAM. O problema era este: no script final, você pode observar a função livre, depois de passar o loop. Nesta iteração, ele ainda não estava. Então eu pensei ...


Mas posso desativar o gc?


Entre as tentativas, criei o gc.disable () e depois de tentar o gc.enable () . Inicio a montagem e o script e recebo:


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

Em geral, a diferença não é grande, então pensei que não havia sentido pagar a mais tente perverter de alguma forma e apenas use o CPython, mas ainda o colete com o Cython.
Provavelmente muitos têm uma pergunta: "O que há com a memória?" O mais surpreendente (não) é que nada. Ela cresceu na mesma proporção e na mesma quantidade. Um artigo veio à mente, mas eu não queria entrar nas fontes do Python. Sim, e isso significava apenas uma coisa - o problema na implementação do vetor.


Final


Após muito tormento com a conversão de tipo, ou seja, que o vetor levaria um ponteiro para um dicionário, o mesmo script resultante foi obtido e com o gc ativado, obtive uma média de 2,6 vezes a diferença (o vetor é mais rápido) e um desempenho de memória relativamente bom.


De repente, ocorreu-me que eu colecionava tudo apenas no Py2.7 e nem tentava fazer nada com o 3.6.


E aqui fiquei realmente surpreso (após os resultados anteriores, a surpresa era 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 

Com tudo isso, o gc ainda funcionou, a memória não devorou ​​e foi o mesmo script. Percebendo que depois de pouco mais de um ano, seria necessário dizer adeus a 2,7, eu ainda me perguntava que havia tanta diferença entre eles. Na maioria das vezes, eu ouvi / li / experimentei e o Py3.6 foi mais lento que o Py2.7. No entanto, os caras dos desenvolvedores do Cython fizeram algo incrível e mudaram a situação pela raiz.


Sumário


Após esse experimento, decidimos não nos preocupar muito com o suporte ao Python 2.7 e refazer qualquer parte dos aplicativos C ++, simplesmente porque não vale a pena. Tudo já foi escrito antes de nós, só podemos usá-lo corretamente para resolver um problema específico.


UPD 24/12/2018:
Seguindo o conselho do iCpu e após ataques laterais, é verificado para não entender o que e como, tentei reescrever a parte do C ++ da maneira mais conveniente para o desenvolvimento no futuro, além de minimizar as abstrações. Acabou ainda pior:


O resultado de pouco conhecimento em 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 

Alguma idéia do que poderia ser melhorado no coparator para que ele funcione mais rápido?

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


All Articles