Cela valait la peine pour moi de toute façon, donc je proposerai ici la solution la plus difficile et la moins élégante pour quiconque pourrait être intéressé. Ma solution consiste à implémenter un algorithme min-max multithread en un seul passage en C ++ et à l'utiliser pour créer un module d'extension Python. Cet effort nécessite un peu de temps système pour apprendre à utiliser les API Python et NumPy C / C ++, et ici je vais montrer le code et donner quelques petites explications et références pour quiconque souhaite emprunter cette voie.
Multi-threadé Min / Max
Il n'y a rien de trop intéressant ici. Le tableau est divisé en morceaux de taille length / workers
. Le min / max est calculé pour chaque morceau dans a future
, qui sont ensuite analysés pour le min / max global.
// 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
Le module d'extension Python
C'est là que les choses commencent à devenir moche ... Une façon d'utiliser le code C ++ en Python est d'implémenter un module d'extension. Ce module peut être construit et installé à l'aide du distutils.core
module standard. Une description complète de ce que cela implique est couverte dans la documentation Python: https://docs.python.org/3/extending/extending.html . REMARQUE: il existe certainement d'autres moyens d'obtenir des résultats similaires, pour citer https://docs.python.org/3/extending/index.html#extending-index :
Ce guide ne couvre que les outils de base pour la création d'extensions fournis dans le cadre de cette version de CPython. Des outils tiers tels que Cython, cffi, SWIG et Numba offrent des approches à la fois plus simples et plus sophistiquées pour créer des extensions C et C ++ pour Python.
Essentiellement, cette voie est probablement plus académique que pratique. Cela étant dit, ce que j'ai fait ensuite, c'était, en m'en tenant assez près du didacticiel, de créer un fichier de module. C'est essentiellement un passe-partout pour que les distutils sachent quoi faire avec votre code et en créer un module Python. Avant de faire quoi que ce soit, il est probablement sage de créer un environnement virtuel Python afin de ne pas polluer vos packages système (voir https://docs.python.org/3/library/venv.html#module-venv ).
Voici le fichier du module:
// 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); }
Dans ce fichier, il y a une utilisation significative de Python ainsi que de l'API NumPy, pour plus d'informations, consultez: https://docs.python.org/3/c-api/arg.html#c.PyArg_ParseTuple , et pour NumPy : https://docs.scipy.org/doc/numpy/reference/c-api.array.html .
Installation du module
La prochaine chose à faire est d'utiliser distutils pour installer le module. Cela nécessite un fichier d'installation:
# 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])
Pour enfin installer le module, exécutez à python3 setup.py install
partir de votre environnement virtuel.
Test du module
Enfin, nous pouvons tester pour voir si l'implémentation C ++ surpasse réellement l'utilisation naïve de NumPy. Pour ce faire, voici un script de test simple:
# 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))
Voici les résultats que j'ai obtenus en faisant tout cela:
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
Celles-ci sont beaucoup moins encourageantes que les résultats indiquent plus tôt dans le fil, qui indiquaient une accélération d'environ 3,5x, et n'intégraient pas le multi-threading. Les résultats que j'ai obtenus sont quelque peu raisonnables, je m'attendrais à ce que la surcharge de threading et domine le temps jusqu'à ce que les tableaux deviennent très grands, auquel point l'augmentation des performances commencerait à se rapprocher de std::thread::hardware_concurrency
x augmenter.
Conclusion
Il y a certainement de la place pour des optimisations spécifiques aux applications pour certains codes NumPy, semble-t-il, en particulier en ce qui concerne le multi-threading. Je ne sais pas si cela en vaut la peine ou non, mais cela semble certainement être un bon exercice (ou quelque chose du genre). Je pense que peut-être apprendre certains de ces "outils tiers" comme Cython peut être une meilleure utilisation du temps, mais qui sait.
amax
etamin