Этот ответ пытается дать оценку того, насколько ускорение может дать параллельная версия. Однако, поскольку эта задача ограничена пропускной способностью памяти (Python -integer-объекты занимают как минимум 32 байта и могут быть разбросаны в памяти, поэтому будет много пропусков кэша), мы не должны ожидать многого.
Первая проблема заключается в том, как обрабатывать ошибки (элемент не является целым числом или значение слишком велико). Я буду придерживаться следующей стратегии / упрощения: когда объект
- не является целым числом,
- является отрицательным целым числом,
- или целым числом>> 2 ^ 30
он будет приведен к специальному номеру (-1
), который сигнализирует о том, что что-то пошло не так. Разрешение только неотрицательных целых чисел <2^30
облегчает мою жизнь, поскольку мне приходится переопределять PyLong_AsLongAndOverflow
, чтобы избежать ошибок, связанных с поднятием, и в противном случае обнаружение переполнений часто бывает громоздким (однако, см. Версию в конце ответ за более сложный подход).
Макет памяти целочисленного объекта Python можно найти здесь :
struct _longobject {
PyObject_VAR_HEAD
digit ob_digit[1];
};
Member ob_size
/ macro Py_SIZE
сообщает нам, сколько 30-битных цифр используется в представлении целого числа (ob_size
отрицательно для отрицательного целого числа).
Таким образом, мое простое правило переводится в следующий C -код (я использую скорее C, чем Cython, поскольку это более простой / более естественный способ использования Python C -API):
#include <Python.h>
// returns -1 if vv is not an integer,
// negative, or > 2**30-1
int to_int(PyObject *vv){
if (PyLong_Check(vv)) {
PyLongObject * v = (PyLongObject *)vv;
Py_ssize_t i = Py_SIZE(v);
if(i==0){
return 0;
}
if(i==1){//small enought for a digit
return v->ob_digit[0];
}
//negative (i<0) or too big (i>1)
return -1;
}
return -1;
}
Сейчас учитывая список, мы можем преобразовать его в int
-буфер параллельно со следующей C -функцией, которая использует omp:
void convert_list(PyListObject *lst, int *output){
Py_ssize_t n = Py_SIZE(lst);
PyObject **data = lst->ob_item;
#pragma omp parallel for
for(Py_ssize_t i=0; i<n; ++i){
output[i] = to_int(data[i]);
}
}
Не так много, чтобы сказать - PyListObject
-API используется для параллельного доступа к элементам списка. Это можно сделать, потому что в функции to_int
нет функции подсчета ссылок / гонок.
Теперь, объединяя все это вместе с Cython:
%%cython -c=-fopenmp --link-args=-fopenmp
import cython
cdef extern from *:
"""
#include <Python.h>
int to_int(PyObject *vv){
... code
}
void convert_list(PyListObject *lst, int *output){
... code
}
"""
void convert_list(list lst, int *output)
@cython.boundscheck(False)
@cython.wraparound(False)
def pack_ints_ead(list int_col):
cdef char[::1] int_buf = bytearray(4*len(int_col))
convert_list(int_col, <int*>(&int_buf[0]))
return int_buf.base
Одна важная деталь: convert_list
не должно быть nogil (потому что это не так)! Omp-потоки и Python -потоки (на которые влияет GIL) - это совершенно разные вещи.
Можно (но не обязательно) освобождать GIL для omp-операций при использовании объектов с buffer-protocol - потому что эти объекты блокируются через буферный протокол и не могут быть изменены из разных Python -threads. У list
нет такого механизма блокировки, и, таким образом, если GIL был освобожден, список можно было бы изменить в других потоках, и все наши указатели могли бы стать недействительными.
Так что теперь к таймингу (с немного большим списком) :
amount = 5*10**7
ints = list(range(amount))
%timeit pack(f'{amount}i', *ints)
# 1.51 s ± 38.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit pack_ints_DavidW(ints)
# 284 ms ± 3.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit pack_ints_ead(ints)
# 177 ms ± 11.8 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Кстати, отключение распараллеливания для pack_ints_ead
приводит к продолжительности работы 209 мс.
Таким образом, учитывая незначительное улучшение ок. 33%, я бы выбрал более надежное решение DavidW.
Вот реализация с несколько иным способом сигнализации неправильных значений:
- , а не целочисленный объект приводит к
-2147483648
(т. Е. 0x80000000
) - наименьшее отрицательное значение, которое может хранить 32-битное целое. - целых чисел
>=2147483647
(т. Е. >=0x7fffffff
) будет отображено / сохранено как 2147483647
- наибольшее положительное число, которое может хранить 32-битное целое число. - целые числа
<=-2147483647
(то есть <=0x80000001
) будет отображено / сохранено как -2147483647
- все остальные целые числа отображаются на их правильное значение .
Основным преимуществом является то, что он работает правильно для большего диапазона целочисленных значений. Этот алгоритм дает почти то же время выполнения (возможно, на 2-3% медленнее), что и первая простая версия:
int to_int(PyObject *vv){
if (PyLong_Check(vv)) {
PyLongObject * v = (PyLongObject *)vv;
Py_ssize_t i = Py_SIZE(v);
int sign = i<0 ? -1 : 1;
i = abs(i);
if(i==0){
return 0;
}
if(i==1){//small enought for a digit
return sign*v->ob_digit[0];
}
if(i==2 && (v->ob_digit[1]>>1)==0){
int add = (v->ob_digit[1]&1) << 30;
return sign*(v->ob_digit[0]+add);
}
return sign * 0x7fffffff;
}
return 0x80000000;
}