Itu sepadan dengan usaha saya, jadi saya akan mengusulkan solusi paling sulit dan paling tidak elegan di sini untuk siapa pun yang mungkin tertarik. Solusi saya adalah mengimplementasikan multi-threaded min-max dalam algoritma one pass di C ++, dan menggunakannya untuk membuat modul ekstensi Python. Upaya ini membutuhkan sedikit overhead untuk mempelajari cara menggunakan Python dan NumPy C / C ++ API, dan di sini saya akan menunjukkan kode dan memberikan beberapa penjelasan dan referensi kecil untuk siapa pun yang ingin menempuh jalur ini.
Min / Maks multi-utas
Tidak ada yang terlalu menarik di sini. Array dipecah menjadi potongan-potongan ukuran length / workers
. Min / max dihitung untuk setiap potongan di a future
, yang kemudian dipindai untuk 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
Modul Ekstensi Python
Di sinilah segalanya mulai menjadi jelek ... Salah satu cara untuk menggunakan kode C ++ dengan Python adalah dengan menerapkan modul ekstensi. Modul ini dapat dibangun dan dipasang menggunakan distutils.core
modul standar. Penjelasan lengkap tentang apa yang diperlukan tercakup dalam dokumentasi Python: https://docs.python.org/3/extending/extending.html . CATATAN: tentunya ada cara lain untuk mendapatkan hasil yang serupa, dengan mengutip https://docs.python.org/3/extending/index.html#extending-index :
Panduan ini hanya mencakup alat dasar untuk membuat ekstensi yang disediakan sebagai bagian dari versi CPython. Alat pihak ketiga seperti Cython, cffi, SWIG dan Numba menawarkan pendekatan yang lebih sederhana dan lebih canggih untuk membuat ekstensi C dan C ++ untuk Python.
Pada dasarnya, rute ini mungkin lebih bersifat akademis daripada praktis. Dengan itu, apa yang saya lakukan selanjutnya adalah, menempel cukup dekat dengan tutorial, membuat file modul. Ini pada dasarnya adalah boilerplate untuk distutils untuk mengetahui apa yang harus dilakukan dengan kode Anda dan membuat modul Python darinya. Sebelum melakukan semua ini, mungkin bijaksana untuk membuat lingkungan virtual Python sehingga Anda tidak mencemari paket sistem Anda (lihat https://docs.python.org/3/library/venv.html#module-venv ).
Ini file modulnya:
// 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); }
Dalam file ini terdapat penggunaan signifikan dari Python serta NumPy API, untuk informasi lebih lanjut lihat: https://docs.python.org/3/c-api/arg.html#c.PyArg_ParseTuple , dan untuk NumPy : https://docs.scipy.org/doc/numpy/reference/c-api.array.html .
Memasang Modul
Hal berikutnya yang harus dilakukan adalah memanfaatkan distutils untuk menginstal modul. Ini membutuhkan file setup:
# 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])
Untuk akhirnya menginstal modul, jalankan python3 setup.py install
dari lingkungan virtual Anda.
Menguji Modul
Terakhir, kita dapat menguji untuk melihat apakah implementasi C ++ benar-benar mengungguli penggunaan NumPy yang naif. Untuk melakukannya, berikut ini skrip pengujian sederhana:
# 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))
Inilah hasil yang saya dapat dari melakukan semua ini:
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
Ini jauh kurang menggembirakan daripada hasil yang ditunjukkan sebelumnya di utas, yang menunjukkan sekitar 3,5x percepatan, dan tidak menyertakan multi-threading. Hasil yang saya capai agak masuk akal, saya berharap bahwa overhead threading dan akan mendominasi waktu sampai array menjadi sangat besar, di mana peningkatan kinerja akan mulai mendekati std::thread::hardware_concurrency
peningkatan x.
Kesimpulan
Jelas ada ruang untuk pengoptimalan khusus aplikasi untuk beberapa kode NumPy, tampaknya, khususnya yang berkaitan dengan multi-threading. Apakah itu sepadan atau tidak, tidak jelas bagi saya, tetapi itu jelas terlihat seperti latihan yang baik (atau sesuatu). Saya pikir mungkin mempelajari beberapa "alat pihak ketiga" seperti Cython mungkin penggunaan waktu yang lebih baik, tapi siapa tahu.
amax
danamin