Die Mühe hat sich für mich sowieso gelohnt, deshalb werde ich hier die schwierigste und am wenigsten elegante Lösung für jeden vorschlagen, der interessiert sein könnte. Meine Lösung besteht darin, einen Multithread-Min-Max-Algorithmus in einem Durchgang in C ++ zu implementieren und damit ein Python-Erweiterungsmodul zu erstellen. Dieser Aufwand erfordert ein wenig Aufwand, um die Verwendung der Python- und NumPy C / C ++ - APIs zu erlernen. Hier werde ich den Code zeigen und einige kleine Erklärungen und Referenzen für alle geben, die diesen Weg beschreiten möchten.
Multithread Min / Max
Hier ist nichts zu interessant. Das Array ist in große Teile unterteilt length / workers
. Das min / max wird für jeden Block in a berechnet future
, der dann nach dem globalen min / max gescannt wird.
// 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
Das Python-Erweiterungsmodul
Hier wird es hässlich ... Eine Möglichkeit, C ++ - Code in Python zu verwenden, besteht darin, ein Erweiterungsmodul zu implementieren. Dieses Modul kann mit dem distutils.core
Standardmodul erstellt und installiert werden . Eine vollständige Beschreibung dessen, was dies bedeutet, finden Sie in der Python-Dokumentation: https://docs.python.org/3/extending/extending.html . HINWEIS: Es gibt sicherlich andere Möglichkeiten, ähnliche Ergebnisse zu erzielen, um https://docs.python.org/3/extending/index.html#extending-index zu zitieren :
Dieses Handbuch behandelt nur die grundlegenden Tools zum Erstellen von Erweiterungen, die als Teil dieser Version von CPython bereitgestellt werden. Tools von Drittanbietern wie Cython, cffi, SWIG und Numba bieten sowohl einfachere als auch komplexere Ansätze zum Erstellen von C- und C ++ - Erweiterungen für Python.
Im Wesentlichen ist dieser Weg wahrscheinlich eher akademisch als praktisch. Nachdem dies gesagt wurde, habe ich als nächstes eine Moduldatei erstellt, indem ich mich ziemlich nah an das Tutorial gehalten habe. Dies ist im Wesentlichen ein Boilerplate, damit Distutils wissen, was mit Ihrem Code zu tun ist, und daraus ein Python-Modul erstellen. Bevor Sie dies tun, ist es wahrscheinlich ratsam, eine virtuelle Python- Umgebung zu erstellen, damit Sie Ihre Systempakete nicht verschmutzen (siehe https://docs.python.org/3/library/venv.html#module-venv ).
Hier ist die Moduldatei:
// 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 dieser Datei werden Python und die NumPy-API in erheblichem Umfang verwendet. Weitere Informationen finden Sie unter: https://docs.python.org/3/c-api/arg.html#c.PyArg_ParseTuple und für NumPy : https://docs.scipy.org/doc/numpy/reference/c-api.array.html .
Modul installieren
Als nächstes müssen Sie distutils verwenden, um das Modul zu installieren. Dies erfordert eine Setup-Datei:
# 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])
Um das Modul endgültig zu installieren, führen Sie es python3 setup.py install
in Ihrer virtuellen Umgebung aus.
Testen des Moduls
Schließlich können wir testen, ob die C ++ - Implementierung tatsächlich die naive Verwendung von NumPy übertrifft. Dazu ein einfaches Testskript:
# 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))
Hier sind die Ergebnisse, die ich dabei erzielt habe:
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
Diese sind weitaus weniger ermutigend als die Ergebnisse früher im Thread, die eine etwa 3,5-fache Beschleunigung anzeigten und kein Multithreading enthielten. Die Ergebnisse, die ich erzielt habe, sind einigermaßen vernünftig. Ich würde erwarten, dass der Aufwand für das Threading und die Zeit dominieren, bis die Arrays sehr groß werden. Ab diesem Zeitpunkt würde sich die Leistungssteigerung dem std::thread::hardware_concurrency
x-Anstieg nähern .
Fazit
Es scheint sicherlich Raum für anwendungsspezifische Optimierungen für einige NumPy-Codes zu geben, insbesondere im Hinblick auf Multithreading. Ob sich die Mühe lohnt oder nicht, ist mir nicht klar, aber es scheint sicherlich eine gute Übung (oder so) zu sein. Ich denke, dass das Erlernen einiger dieser "Tools von Drittanbietern" wie Cython möglicherweise eine bessere Zeitnutzung darstellt, aber wer weiß.
amax
undamin