Dù sao thì cũng đáng cho tôi nỗ lực, vì vậy tôi sẽ đề xuất giải pháp khó nhất và kém thanh lịch nhất ở đây cho những ai có thể quan tâm. Giải pháp của tôi là triển khai thuật toán min-max đa luồng trong một lần vượt qua trong C ++ và sử dụng thuật toán này để tạo mô-đun mở rộng Python. Nỗ lực này đòi hỏi một chút chi phí để học cách sử dụng các API Python và NumPy C / C ++, và ở đây tôi sẽ hiển thị mã và đưa ra một số giải thích và tham chiếu nhỏ cho những ai muốn đi theo con đường này.
Đa luồng Min / Max
Không có gì quá thú vị ở đây. Mảng được chia thành nhiều phần có kích thước length / workers
. Tối thiểu / tối đa được tính cho từng đoạn trong a future
, sau đó được quét để tìm giá trị tối thiểu / tối đa chung.
// 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
Mô-đun mở rộng Python
Đây là nơi mọi thứ bắt đầu trở nên tồi tệ ... Một cách để sử dụng mã C ++ trong Python là triển khai một mô-đun mở rộng. Mô-đun này có thể được xây dựng và cài đặt bằng distutils.core
mô-đun tiêu chuẩn. Mô tả đầy đủ về những gì điều này đòi hỏi được đề cập trong tài liệu Python: https://docs.python.org/3/exnking/exfining.html . LƯU Ý: chắc chắn có nhiều cách khác để có được kết quả tương tự, trích dẫn https://docs.python.org/3/exnking/index.html#exfining-index :
Hướng dẫn này chỉ bao gồm các công cụ cơ bản để tạo tiện ích mở rộng được cung cấp như một phần của phiên bản CPython này. Các công cụ của bên thứ ba như Cython, cffi, SWIG và Numba cung cấp các cách tiếp cận đơn giản hơn và phức tạp hơn để tạo các phần mở rộng C và C ++ cho Python.
Về cơ bản, lộ trình này có lẽ mang tính học thuật hơn là thực tế. Như đã nói, điều tôi làm tiếp theo là bám sát hướng dẫn, tạo một tệp mô-đun. Về cơ bản, đây là bản soạn sẵn cho các bản phân phối để biết phải làm gì với mã của bạn và tạo một mô-đun Python từ nó. Trước khi thực hiện bất kỳ điều gì trong số này, điều khôn ngoan là nên tạo một môi trường ảo Python để bạn không làm ô nhiễm các gói hệ thống của mình (xem https://docs.python.org/3/library/venv.html#module-venv ).
Đây là tệp mô-đun:
// 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); }
Trong tệp này, có một cách sử dụng đáng kể Python cũng như API NumPy, để biết thêm thông tin, hãy tham khảo: https://docs.python.org/3/c-api/arg.html#c.PyArg_ParseTuple và cho NumPy : https://docs.scipy.org/doc/numpy/reference/c-api.array.html .
Cài đặt mô-đun
Điều tiếp theo cần làm là sử dụng các bản phân phối để cài đặt mô-đun. Điều này yêu cầu một tệp thiết lập:
# 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])
Để cuối cùng cài đặt mô-đun, hãy thực thi python3 setup.py install
từ môi trường ảo của bạn.
Kiểm tra mô-đun
Cuối cùng, chúng ta có thể kiểm tra xem việc triển khai C ++ có thực sự vượt trội hơn việc sử dụng NumPy một cách ngây thơ hay không. Để làm như vậy, đây là một kịch bản thử nghiệm đơn giản:
# 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))
Đây là kết quả tôi nhận được từ việc làm tất cả những điều này:
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
Đây là những kết quả không đáng khích lệ hơn nhiều so với kết quả chỉ ra trước đó trong luồng, chỉ ra đâu đó xung quanh tốc độ tăng gấp 3,5 lần và không kết hợp đa luồng. Kết quả mà tôi đạt được có phần hợp lý, tôi mong đợi rằng chi phí phân luồng và sẽ chiếm ưu thế về thời gian cho đến khi các mảng trở nên rất lớn, tại thời điểm đó, mức tăng hiệu suất sẽ bắt đầu tiếp cận với std::thread::hardware_concurrency
mức tăng x.
Phần kết luận
Chắc chắn có chỗ cho các tối ưu hóa ứng dụng cụ thể đối với một số mã NumPy, có vẻ như, đặc biệt là liên quan đến đa luồng. Tôi không rõ liệu nó có xứng đáng với nỗ lực hay không, nhưng chắc chắn nó có vẻ như là một bài tập tốt (hoặc một cái gì đó). Tôi nghĩ rằng có lẽ học một số "công cụ của bên thứ ba" như Cython có thể sử dụng thời gian tốt hơn, nhưng ai biết được.
amax
vàamin