De todos modos, valió la pena el esfuerzo para mí, así que propondré aquí la solución más difícil y menos elegante para quien pueda estar interesado. Mi solución es implementar un mínimo-máximo de subprocesos múltiples en un algoritmo de un paso en C ++, y usarlo para crear un módulo de extensión de Python. Este esfuerzo requiere un poco de sobrecarga para aprender a usar las API de Python y NumPy C / C ++, y aquí mostraré el código y daré algunas pequeñas explicaciones y referencias para quien desee seguir este camino.
Min / Max multiproceso
No hay nada demasiado interesante aquí. La matriz se divide en trozos de tamaño length / workers. El mínimo / máximo se calcula para cada fragmento en a future, que luego se escanea para el mínimo / máximo 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
El módulo de extensión de Python
Aquí es donde las cosas empiezan a ponerse feas ... Una forma de usar el código C ++ en Python es implementar un módulo de extensión. Este módulo se puede construir e instalar utilizando el distutils.coremódulo estándar. Una descripción completa de lo que esto implica se cubre en la documentación de Python: https://docs.python.org/3/extending/extending.html . NOTA: ciertamente hay otras formas de obtener resultados similares, por citar https://docs.python.org/3/extending/index.html#extending-index :
Esta guía solo cubre las herramientas básicas para crear extensiones proporcionadas como parte de esta versión de CPython. Las herramientas de terceros como Cython, cffi, SWIG y Numba ofrecen enfoques más simples y sofisticados para crear extensiones C y C ++ para Python.
Esencialmente, esta ruta es probablemente más académica que práctica. Habiendo dicho eso, lo que hice a continuación fue, manteniéndome bastante cerca del tutorial, crear un archivo de módulo. Esto es esencialmente un texto estándar para que los distutils sepan qué hacer con su código y creen un módulo de Python a partir de él. Antes de hacer algo de esto, probablemente sea aconsejable crear un entorno virtual de Python para no contaminar los paquetes de su sistema (consulte https://docs.python.org/3/library/venv.html#module-venv ).
Aquí está el archivo del módulo:
// 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); }
En este archivo hay un uso significativo tanto de Python como de la API de NumPy, para más información consultar: https://docs.python.org/3/c-api/arg.html#c.PyArg_ParseTuple , y para NumPy : https://docs.scipy.org/doc/numpy/reference/c-api.array.html .
Instalación del módulo
Lo siguiente que debe hacer es utilizar distutils para instalar el módulo. Esto requiere un archivo de instalación:
# 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])
Para finalmente instalar el módulo, ejecute python3 setup.py install desde su entorno virtual.
Prueba del módulo
Finalmente, podemos probar para ver si la implementación de C ++ realmente supera al uso ingenuo de NumPy. Para hacerlo, aquí hay un script de prueba 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))
Estos son los resultados que obtuve al hacer todo esto:
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
Estos son mucho menos alentadores de lo que los resultados indican anteriormente en el hilo, que indicaron en algún lugar una aceleración de alrededor de 3.5x, y no incorporaron subprocesos múltiples. Los resultados que obtuve son algo razonables, esperaría que la sobrecarga de subprocesos y dominaría el tiempo hasta que las matrices se volvieran muy grandes, momento en el que el aumento de rendimiento comenzaría a acercarse a std::thread::hardware_concurrencyx aumento.
Conclusión
Ciertamente, parece que hay espacio para optimizaciones específicas de aplicaciones para algunos códigos de NumPy, en particular con respecto al subproceso múltiple. No tengo claro si vale la pena el esfuerzo o no, pero ciertamente parece un buen ejercicio (o algo así). Creo que quizás aprender algunas de esas "herramientas de terceros" como Cython puede ser un mejor uso del tiempo, pero quién sabe.
amaxamin