Ne è valsa comunque la pena per me, quindi proporrò qui la soluzione più difficile e meno elegante per chiunque possa essere interessato. La mia soluzione è implementare un algoritmo min-max multi-thread in un passaggio in C ++ e usarlo per creare un modulo di estensione Python. Questo sforzo richiede un po 'di overhead per imparare a usare le API C / C ++ di Python e NumPy, e qui mostrerò il codice e fornirò alcune piccole spiegazioni e riferimenti per chiunque desideri seguire questo percorso.
Multi-threaded Min / Max
Non c'è niente di troppo interessante qui. L'array è suddiviso in blocchi di dimensioni length / workers
. Il min / max viene calcolato per ogni blocco in a future
, che viene quindi scansionato per il min / max globale.
// mt_np.cc
//
// multi-threaded min/max algorithm
#include <algorithm>
#include <future>
#include <vector>
namespace mt_np {
/*
* Get {min,max} in interval [begin,end)
*/
template <typename T> std::pair<T, T> min_max(T *begin, T *end) {
T min{*begin};
T max{*begin};
while (++begin < end) {
if (*begin < min) {
min = *begin;
continue;
} else if (*begin > max) {
max = *begin;
}
}
return {min, max};
}
/*
* get {min,max} in interval [begin,end) using #workers for concurrency
*/
template <typename T>
std::pair<T, T> min_max_mt(T *begin, T *end, int workers) {
const long int chunk_size = std::max((end - begin) / workers, 1l);
std::vector<std::future<std::pair<T, T>>> min_maxes;
// fire up the workers
while (begin < end) {
T *next = std::min(end, begin + chunk_size);
min_maxes.push_back(std::async(min_max<T>, begin, next));
begin = next;
}
// retrieve the results
auto min_max_it = min_maxes.begin();
auto v{min_max_it->get()};
T min{v.first};
T max{v.second};
while (++min_max_it != min_maxes.end()) {
v = min_max_it->get();
min = std::min(min, v.first);
max = std::max(max, v.second);
}
return {min, max};
}
}; // namespace mt_np
Il modulo di estensione Python
Qui è dove le cose iniziano a diventare brutte ... Un modo per usare il codice C ++ in Python è implementare un modulo di estensione. Questo modulo può essere costruito e installato utilizzando il distutils.core
modulo standard. Una descrizione completa di ciò che ciò comporta è trattata nella documentazione di Python: https://docs.python.org/3/extending/extending.html . NOTA: ci sono sicuramente altri modi per ottenere risultati simili, per citare https://docs.python.org/3/extending/index.html#extending-index :
Questa guida copre solo gli strumenti di base per la creazione di estensioni fornite come parte di questa versione di CPython. Strumenti di terze parti come Cython, cffi, SWIG e Numba offrono approcci più semplici e sofisticati alla creazione di estensioni C e C ++ per Python.
In sostanza, questo percorso è probabilmente più accademico che pratico. Detto questo, quello che ho fatto dopo è stato, rimanendo abbastanza vicino al tutorial, creare un file di modulo. Questo è essenzialmente boilerplate per distutils per sapere cosa fare con il tuo codice e creare un modulo Python da esso. Prima di fare tutto ciò è probabilmente saggio creare un ambiente virtuale Python in modo da non inquinare i pacchetti di sistema (vedere https://docs.python.org/3/library/venv.html#module-venv ).
Ecco il file del modulo:
// mt_np_forpy.cc
//
// C++ module implementation for multi-threaded min/max for np
#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION
#include <python3.6/numpy/arrayobject.h>
#include "mt_np.h"
#include <cstdint>
#include <iostream>
using namespace std;
/*
* check:
* shape
* stride
* data_type
* byteorder
* alignment
*/
static bool check_array(PyArrayObject *arr) {
if (PyArray_NDIM(arr) != 1) {
PyErr_SetString(PyExc_RuntimeError, "Wrong shape, require (1,n)");
return false;
}
if (PyArray_STRIDES(arr)[0] != 8) {
PyErr_SetString(PyExc_RuntimeError, "Expected stride of 8");
return false;
}
PyArray_Descr *descr = PyArray_DESCR(arr);
if (descr->type != NPY_LONGLTR && descr->type != NPY_DOUBLELTR) {
PyErr_SetString(PyExc_RuntimeError, "Wrong type, require l or d");
return false;
}
if (descr->byteorder != '=') {
PyErr_SetString(PyExc_RuntimeError, "Expected native byteorder");
return false;
}
if (descr->alignment != 8) {
cerr << "alignment: " << descr->alignment << endl;
PyErr_SetString(PyExc_RuntimeError, "Require proper alignement");
return false;
}
return true;
}
template <typename T>
static PyObject *mt_np_minmax_dispatch(PyArrayObject *arr) {
npy_intp size = PyArray_SHAPE(arr)[0];
T *begin = (T *)PyArray_DATA(arr);
auto minmax =
mt_np::min_max_mt(begin, begin + size, thread::hardware_concurrency());
return Py_BuildValue("(L,L)", minmax.first, minmax.second);
}
static PyObject *mt_np_minmax(PyObject *self, PyObject *args) {
PyArrayObject *arr;
if (!PyArg_ParseTuple(args, "O", &arr))
return NULL;
if (!check_array(arr))
return NULL;
switch (PyArray_DESCR(arr)->type) {
case NPY_LONGLTR: {
return mt_np_minmax_dispatch<int64_t>(arr);
} break;
case NPY_DOUBLELTR: {
return mt_np_minmax_dispatch<double>(arr);
} break;
default: {
PyErr_SetString(PyExc_RuntimeError, "Unknown error");
return NULL;
}
}
}
static PyObject *get_concurrency(PyObject *self, PyObject *args) {
return Py_BuildValue("I", thread::hardware_concurrency());
}
static PyMethodDef mt_np_Methods[] = {
{"mt_np_minmax", mt_np_minmax, METH_VARARGS, "multi-threaded np min/max"},
{"get_concurrency", get_concurrency, METH_VARARGS,
"retrieve thread::hardware_concurrency()"},
{NULL, NULL, 0, NULL} /* sentinel */
};
static struct PyModuleDef mt_np_module = {PyModuleDef_HEAD_INIT, "mt_np", NULL,
-1, mt_np_Methods};
PyMODINIT_FUNC PyInit_mt_np() { return PyModule_Create(&mt_np_module); }
In questo file c'è un uso significativo dell'API Python e NumPy, per maggiori informazioni consultare: https://docs.python.org/3/c-api/arg.html#c.PyArg_ParseTuple e per NumPy : https://docs.scipy.org/doc/numpy/reference/c-api.array.html .
Installazione del modulo
La prossima cosa da fare è utilizzare distutils per installare il modulo. Ciò richiede un file di installazione:
# setup.py
from distutils.core import setup,Extension
module = Extension('mt_np', sources = ['mt_np_module.cc'])
setup (name = 'mt_np',
version = '1.0',
description = 'multi-threaded min/max for np arrays',
ext_modules = [module])
Per installare finalmente il modulo, esegui python3 setup.py install
dal tuo ambiente virtuale.
Test del modulo
Infine, possiamo verificare se l'implementazione di C ++ supera effettivamente l'uso ingenuo di NumPy. Per farlo, ecco un semplice script di test:
# timing.py
# compare numpy min/max vs multi-threaded min/max
import numpy as np
import mt_np
import timeit
def normal_min_max(X):
return (np.min(X),np.max(X))
print(mt_np.get_concurrency())
for ssize in np.logspace(3,8,6):
size = int(ssize)
print('********************')
print('sample size:', size)
print('********************')
samples = np.random.normal(0,50,(2,size))
for sample in samples:
print('np:', timeit.timeit('normal_min_max(sample)',
globals=globals(),number=10))
print('mt:', timeit.timeit('mt_np.mt_np_minmax(sample)',
globals=globals(),number=10))
Ecco i risultati che ho ottenuto facendo tutto questo:
8
********************
sample size: 1000
********************
np: 0.00012079699808964506
mt: 0.002468645994667895
np: 0.00011947099847020581
mt: 0.0020772050047526136
********************
sample size: 10000
********************
np: 0.00024697799381101504
mt: 0.002037393998762127
np: 0.0002713389985729009
mt: 0.0020942929986631498
********************
sample size: 100000
********************
np: 0.0007130410012905486
mt: 0.0019842900001094677
np: 0.0007540129954577424
mt: 0.0029724110063398257
********************
sample size: 1000000
********************
np: 0.0094779249993735
mt: 0.007134920000680722
np: 0.009129883001151029
mt: 0.012836456997320056
********************
sample size: 10000000
********************
np: 0.09471094200125663
mt: 0.0453535050037317
np: 0.09436299200024223
mt: 0.04188535599678289
********************
sample size: 100000000
********************
np: 0.9537652180006262
mt: 0.3957935369980987
np: 0.9624398809974082
mt: 0.4019058070043684
Questi sono molto meno incoraggianti di quanto i risultati indicano in precedenza nel thread, che indicava un aumento della velocità di circa 3,5 volte e non incorporavano il multi-threading. I risultati che ho ottenuto sono in qualche modo ragionevoli, mi aspetterei che il sovraccarico del threading dominasse il tempo fino a quando gli array non diventassero molto grandi, a quel punto l'aumento delle prestazioni inizierebbe ad avvicinarsi std::thread::hardware_concurrency
all'incremento x.
Conclusione
C'è sicuramente spazio per ottimizzazioni specifiche dell'applicazione per alcuni codici NumPy, sembrerebbe, in particolare per quanto riguarda il multi-threading. Se ne valga la pena o meno non mi è chiaro, ma certamente mi sembra un buon esercizio (o qualcosa del genere). Penso che forse imparare alcuni di quegli "strumenti di terze parti" come Cython possa essere un uso migliore del tempo, ma chi lo sa.
amax
amin