어쨌든 저에게는 노력할만한 가치가 있었으므로 관심이있는 사람을 위해 여기에서 가장 어렵고 덜 우아한 해결책을 제안 할 것입니다. 내 솔루션은 C ++의 한 패스 알고리즘에서 다중 스레드 최소-최대를 구현하고 이것을 사용하여 Python 확장 모듈을 만드는 것입니다. 이 작업에는 Python 및 NumPy C / C ++ API 사용 방법을 배우는 데 약간의 오버 헤드가 필요하며, 여기에서는 코드를 보여주고이 경로를 따르고 자하는 사람을위한 간단한 설명과 참조를 제공합니다.
다중 스레드 최소 / 최대
여기에는 너무 흥미로운 것이 없습니다. 배열은 크기의 덩어리로 나뉩니다 length / workers
. 의 각 청크에 대해 최소 / 최대가 계산 future
된 다음 전역 최소 / 최대에 대해 스캔됩니다.
// 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
Python 확장 모듈
여기에서 상황이 추악 해지기 시작합니다 ... Python에서 C ++ 코드를 사용하는 한 가지 방법은 확장 모듈을 구현하는 것입니다. 이 모듈은 distutils.core
표준 모듈을 사용하여 구축 및 설치할 수 있습니다 . 이에 수반되는 내용에 대한 전체 설명은 Python 문서 https://docs.python.org/3/extending/extending.html 에서 다룹니다 . 참고 : https://docs.python.org/3/extending/index.html#extending-index 를 인용하여 유사한 결과를 얻을 수있는 다른 방법이 있습니다 .
이 가이드는이 버전의 CPython의 일부로 제공되는 확장을 만들기위한 기본 도구 만 다룹니다. Cython, cffi, SWIG 및 Numba와 같은 타사 도구는 Python 용 C 및 C ++ 확장을 만드는 데 더 간단하고 정교한 접근 방식을 제공합니다.
본질적으로이 경로는 아마도 실용적 이라기보다는 학문적 일 것입니다. 그 말을 듣고 다음에 내가 한 일은 튜토리얼에 매우 가깝게 모듈 파일을 만드는 것입니다. 이것은 본질적으로 distutils가 여러분의 코드로 무엇을해야하는지 알고 파이썬 모듈을 생성하기위한 상용구입니다. 이 작업을 수행하기 전에 시스템 패키지를 오염시키지 않도록 Python 가상 환경 을 만드는 것이 현명 할 것입니다 ( https://docs.python.org/3/library/venv.html#module-venv 참조 ).
다음은 모듈 파일입니다.
// 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); }
이 파일에는 NumPy API뿐만 아니라 Python이 많이 사용됩니다. 자세한 내용은 https://docs.python.org/3/c-api/arg.html#c.PyArg_ParseTuple 및 NumPy를 참조하십시오. : https://docs.scipy.org/doc/numpy/reference/c-api.array.html .
모듈 설치
다음으로 할 일은 distutils를 사용하여 모듈을 설치하는 것입니다. 여기에는 설정 파일이 필요합니다.
# 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])
마지막으로 모듈을 설치하려면 python3 setup.py install
가상 환경에서 실행 하십시오.
모듈 테스트
마지막으로 C ++ 구현이 실제로 NumPy의 순진한 사용을 능가하는지 테스트 할 수 있습니다. 이를 위해 다음은 간단한 테스트 스크립트입니다.
# 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))
이 모든 작업을 통해 얻은 결과는 다음과 같습니다.
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
이는 스레드의 초기에 나타난 결과보다 훨씬 덜 고무적입니다. 이는 약 3.5 배의 속도 향상을 나타내며 멀티 스레딩을 통합하지 않았습니다. 내가 달성 한 결과는 다소 합리적이며, 스레딩 오버 헤드가 어레이가 매우 커질 때까지 시간을 지배 할 것으로 예상되며,이 시점에서 성능 증가가 std::thread::hardware_concurrency
x 증가 에 가까워지기 시작할 것 입니다.
결론
일부 NumPy 코드에 대한 애플리케이션 특정 최적화의 여지가 있습니다. 특히 멀티 스레딩과 관련하여 보일 것입니다. 노력할 가치가 있는지 여부는 분명하지 않지만 확실히 좋은 운동 (또는 무언가)처럼 보입니다. Cython과 같은 "타사 도구"중 일부를 배우는 것이 시간을 더 잘 활용할 수 있다고 생각하지만 누가 알겠습니까?
amax
과amin