Передача 3-мерного массива numpy на C
Я пишу расширение C для своей программы Python для достижения скорости и запускаю какое-то очень странное поведение, пытающееся перейти в 3-мерный массив numpy. Он работает с 2-мерным массивом, но я уверен, что я что-то прикрутил указателями, пытаясь заставить его работать с 3-м измерением. Но вот странная часть. Если я просто перейду в трехмерный массив, он выйдет из строя с Ошибка шины. Если (в Python) я сначала создаю свою переменную как 2D-массив, а затем перезаписываю ее с помощью 3D-массива, работает отлично. Если переменная является пустым массивом сначала, а затем 3D-массив, он падает с Seg Fault. Как это может случиться?
Кроме того, может ли кто-нибудь помочь мне получить 3D-массив? Или я должен просто сдаться и перейти в 2D-массив и изменить его сам?
Здесь мой код C:
static PyObject* func(PyObject* self, PyObject* args) {
PyObject *list2_obj;
PyObject *list3_obj;
if (!PyArg_ParseTuple(args, "OO", &list2_obj, &list3_obj))
return NULL;
double **list2;
double ***list3;
//Create C arrays from numpy objects:
int typenum = NPY_DOUBLE;
PyArray_Descr *descr;
descr = PyArray_DescrFromType(typenum);
npy_intp dims[3];
if (PyArray_AsCArray(&list2_obj, (void **)&list2, dims, 2, descr) < 0 || PyArray_AsCArray(&list3_obj, (void ***)&list3, dims, 3, descr) < 0) {
PyErr_SetString(PyExc_TypeError, "error converting to c array");
return NULL;
}
printf("2D: %f, 3D: %f.\n", list2[3][1], list3[1][0][2]);
}
И вот мой код Python, который вызывает указанную выше функцию:
import cmod, numpy
l2 = numpy.array([[1.0,2.0,3.0], [4.0,5.0,6.0], [7.0,8.0,9.0], [3.0, 5.0, 0.0]])
l3 = numpy.array([[2,7, 1], [6, 3, 9], [1, 10, 13], [4, 2, 6]]) # Line A
l3 = numpy.array([]) # Line B
l3 = numpy.array([[[2,7, 1, 11], [6, 3, 9, 12]],
[[1, 10, 13, 15], [4, 2, 6, 2]]])
cmod.func(l2, l3)
Итак, если я прокомментирую обе строки A и B, он сбой с ошибкой шины. Если строка A есть, но строка B закомментирована, она работает правильно, без ошибок. Если строка B есть, но строка A закомментирована, она печатает правильные номера, но затем Seg faults. Наконец, если обе строки присутствуют, они также печатают правильные цифры, а затем Seg faults. Что, черт возьми, здесь происходит?
EDIT: Хорошо. Вау. Поэтому я использовал int
в Python, но называет их double
в C. И это отлично работает с 1D и 2D массивами. Но не 3D. Поэтому я изменил определение Python l3 на float, и теперь все работает фантастически (Большое спасибо Bi Rico).
Но теперь более странное поведение с линиями A и B! Теперь, если обе строки закомментированы, программа работает. Если присутствует строка B, но A закомментирован, она работает, а если обе раскоментированы. Но если строка A присутствует и B закомментирован, я снова получаю эту фантастическую ошибку шины. Я бы очень хотел избежать этого в будущем, так ли кто-нибудь знает, почему объявление переменной Python может иметь такой эффект?
РЕДАКТИРОВАТЬ 2: Ну, как сумасшедшие, как эти ошибки, все они связаны с 3-мерным массивом numpy, в который я вхожу. Если я только перехожу в 1- или 2-D массивы, он ведет себя так, как ожидалось, а манипуляции с другими переменными Python ничего не делает. Это заставляет меня думать, что проблема лежит где-то в подсчете ссылок на Python. В C-коде счетчик ссылок уменьшается больше, чем нужно для трехмерных массивов, и когда эта функция возвращает Python пытается очистить объекты и пытается удалить указатель NULL. Это только моя догадка, и я попытался Py_INCREF();
все, что я мог придумать безрезультатно. Думаю, я просто буду использовать 2D-массив и переделать его в C.
Ответы
Ответ 1
Я уже упоминал об этом в комментарии, но, надеюсь, его немного промыть поможет сделать его более понятным.
Когда вы работаете с массивами numpy в C, полезно четко указывать набор ваших массивов. В частности, похоже, что вы указываете свои указатели как double ***list3
, но как вы создаете l3
в своем коде на python, вы получите массив с dtype npy_intp
(я думаю). Вы можете исправить это, явно используя dtype при создании своих массивов.
import cmod, numpy
l2 = numpy.array([[1.0,2.0,3.0],
[4.0,5.0,6.0],
[7.0,8.0,9.0],
[3.0, 5.0, 0.0]], dtype="double")
l3 = numpy.array([[[2,7, 1, 11], [6, 3, 9, 12]],
[[1, 10, 13, 15], [4, 2, 6, 2]]], dtype="double")
cmod.func(l2, l3)
Другое примечание: из-за того, как работает python, почти невозможно, чтобы строки "A" и "строка B" имели какое-либо влияние на код C, что так всегда. Я знаю, что это, похоже, противоречит вашему эмпирическому опыту, но я уверен в этом.
Я немного менее уверен в этом, но основанный на моем опыте с C, ошибки шины и segfaults не детерминированы. Они зависят от распределения памяти, выравнивания и адресов. В какой-то ситуации код, кажется, работает нормально 10 раз и не работает на 11-м запуске, хотя ничего не изменилось.
Считаете ли вы использование cython? Я знаю, что это не вариант для всех, но если это вариант, вы можете получить почти ускорение на уровне C, используя типизированные просмотры памяти.
Ответ 2
Вместо преобразования в массив c-style я обычно обращаюсь к элементам массива numpy непосредственно с помощью PyArray_GETPTR
(см. http://docs.scipy.org/doc/numpy/reference/c-api.array.html#data-access).
Например, для доступа к элементу трехмерного массива numpy типа double use
double elem=*((double *)PyArray_GETPTR3(list3_obj,i,j,k))
.
Для вашего приложения вы можете определить правильное количество измерений для каждого массива с помощью PyArray_NDIM
, затем получить доступ к элементам, используя соответствующую версию PyArray_GETPTR
.
Ответ 3
Согласно http://docs.scipy.org/doc/numpy/reference/c-api.array.html?highlight=pyarray_ascarray#PyArray_AsCArray:
Примечание. Моделирование массива C-стиля не является полным для массивов 2-го и 3-мерного. Например, моделируемые массивы указателей не могут быть переданы подпрограммам, ожидающим конкретные, статически заданные массивы с 2-го и 3-мерным массивами. Чтобы перейти к функциям, требующим такого рода входов, вы должны статически определить требуемый массив и скопировать данные.
Я думаю, что это означает, что PyArray_AsCArray
возвращает блок памяти с данными в нем в порядке С. Однако для доступа к этим данным требуется дополнительная информация (см. http://www.phy225.dept.shef.ac.uk/mediawiki/index.php/Arrays,_dynamic_array_allocation). Это можно достичь, зная размеры заблаговременно, объявив массив, а затем скопировав данные в нужном порядке. Однако я подозреваю, что более общий случай более полезен: вы не знаете размеры до тех пор, пока они не будут возвращены. Я думаю, что следующий код создаст необходимую C-указательную структуру C, чтобы разрешить обработку данных.
static PyObject* func(PyObject* self, PyObject* args) {
PyObject *list2_obj;
PyObject *list3_obj;
if (!PyArg_ParseTuple(args, "OO", &list2_obj, &list3_obj)) return NULL;
double **list2;
double ***list3;
// For the final version
double **final_array2;
double **final_array2;
// For loops
int i,j;
//Create C arrays from numpy objects:
int typenum = NPY_DOUBLE;
PyArray_Descr *descr;
descr = PyArray_DescrFromType(typenum);
// One per array coming back ...
npy_intp dims2[2];
npy_intp dims3[3];
if (PyArray_AsCArray(&list2_obj, (void **)&list2, dims2, 2, descr) < 0 || PyArray_AsCArray(&list3_obj, (void ***)&list3, dims3, 3, descr) < 0) {
PyErr_SetString(PyExc_TypeError, "error converting to c array");
return NULL;
}
// Create the pointer arrays needed to access the data
// 2D array
final_array2 = calloc(dim2[0], sizeof(double *));
for (i=0; i<dim[0]; i++) final_array2[i] = list2 + dim2[1]*sizeof(double);
// 2D array
final_array3 = calloc(dim3[0], sizeof(double **));
final_array3[0] = calloc(dim3[0]*dim3[1], sizeof(double *));
for (i=0; i<dim[0]; i++) {
final_array3[i] = list2 + dim3[1]*sizeof(double *);
for (j=0; j<dim[1]; j++) {
final_array[i][j] = final_array[i] + dim3[2]*sizeof(double);
}
}
printf("2D: %f, 3D: %f.\n", final_array2[3][1], final_array3[1][0][2]);
// Do stuff with the arrays
// When ready to complete, free the array access stuff
free(final_array2);
free(final_array3[0]);
free(final_array3);
// I would guess you also need to free the stuff allocated by PyArray_AsCArray, if so:
free(list2);
free(list3);
}
Я не мог найти определение для npy_intp
, это предполагает, что оно совпадает с int
. Если это не так, вам нужно будет преобразовать dim2
и dim3
в int
массивы, прежде чем делать код.
Ответ 4
В числовом C-API была ошибка, которая теперь должна быть исправлена:
https://github.com/numpy/numpy/pull/5314