Ответ 1
Вкратце: получение элемента из массива NumPy требует создания новых объектов Python, в то время как это не относится к спискам. Кроме того, индексирование более немного сложнее для массивов NumPy, чем списки, которые могут добавить некоторые дополнительные служебные данные.
Чтобы повторить, операции NumPy, которые вы указали, выполняют следующие действия:
-
b[100][100]
возвращает строку 100 изb
в качестве массива, а затем получает значение в индексе 100 этой строки, возвращая значение как объект (например, типnp.int64
). -
b[100,100]
возвращает значение в строке 100 и столбце 100 напрямую (первый промежуточный массив не возвращается). -
b.item(100,100)
делает то же самое, что и вышеb[100,100]
, за исключением того, что значение преобразуется в родной тип Python и возвращается.
Теперь из этой операции (1) является самым медленным, поскольку для этого требуются две последовательные операции индексирования NumPy (я объясню, почему это медленнее индексации списка ниже). (2) выполняется быстрее, поскольку выполняется только одна операция индексации. Операция (3) возможно медленнее, поскольку это вызов метода (в Python они обычно медленны).
Почему доступ к списку еще быстрее, чем b[100,100]
?
Создание объекта
Списки Python представляют собой массивы указателей на объекты в памяти. Например, список [1, 2, 3]
не содержит эти целые числа напрямую, а указатели на адреса памяти - это все целые объекты. Чтобы получить элемент из списка, Python просто возвращает ссылку на объект.
Массивы NumPy не являются коллекциями объектов. Массив np.array([1, 2, 3])
является просто непрерывным блоком памяти с битами, установленными для представления этих целых значений. Чтобы получить целое число из этого массива, новый объект Python должен быть сконструирован в памяти отдельно от массива. Например, объект np.int64
может быть возвращен операцией индексирования: этот объект ранее не существовал и должен был быть создан.
Сложность индексирования
Две другие причины, по которым a[100][100]
(получение из списка) быстрее, чем b[100,100]
(получение из массива), таковы:
-
Операционный код байта
BINARY_SUBSCR
выполняется при индексировании обоих списков и массивов, но оптимизирован для списка Python. -
Внутренняя C-функция, обрабатывающая целочисленную индексацию для списков Python, очень короткая и простая. С другой стороны, индексирование NumPy намного сложнее и выполняется значительное количество кода для определения используемого типа индексации, чтобы можно было вернуть правильное значение.
Ниже описаны шаги для доступа к элементам в списке и массиве с a[100][100]
и b[100,100]
.
Bytecode
Для четырех списков и массивов запускаются те же четыре кода кода байт-кода:
0 LOAD_NAME 0 (a) # the list or array
3 LOAD_CONST 0 (100) # index number (tuple for b[100,100])
6 BINARY_SUBSCR # find correct "getitem" function
7 RETURN_VALUE # value returned from list or array
Примечание: если вы начинаете индексирование цепей для многомерных списков, например. a[100][100][100]
, вы начинаете повторять эти инструкции байткода. Это не происходит для массивов NumPy с использованием индексации кортежей: b[100,100,100]
использует только четыре команды. Вот почему зазор в таймингах начинает закрываться по мере увеличения количества измерений.
Поиск правильной функции "getitem"
Функции для доступа к спискам и массивам различны, и в каждом случае необходимо найти правильный. Эта задача обрабатывается BINARY_SUBSCR
opcode:
w = POP(); // our index
v = TOP(); // our list or NumPy array
if (PyList_CheckExact(v) && PyInt_CheckExact(w)) { // do we have a list and an int?
/* INLINE: list[int] */
Py_ssize_t i = PyInt_AsSsize_t(w);
if (i < 0)
i += PyList_GET_SIZE(v);
if (i >= 0 && i < PyList_GET_SIZE(v)) {
x = PyList_GET_ITEM(v, i); // call "getitem" for lists
Py_INCREF(x);
}
else
goto slow_get;
}
else
slow_get:
x = PyObject_GetItem(v, w); // else, call another function
// to work out what is needed
Py_DECREF(v);
Py_DECREF(w);
SET_TOP(x);
if (x != NULL) continue;
break;
Этот код оптимизирован для списков Python. Если функция увидит список, она быстро вызовет функцию PyList_GET_ITEM
. Теперь этот список можно получить по требуемому индексу (см. Следующий раздел ниже).
Однако, если он не видит список (например, у нас есть массив NumPy), он принимает путь "slow_get". Это, в свою очередь, вызывает другую функцию PyObject_GetItem
, чтобы проверить, какая функция "getitem" отображает объект:
PyObject_GetItem(PyObject *o, PyObject *key)
{
PyMappingMethods *m;
if (o == NULL || key == NULL)
return null_error();
m = o->ob_type->tp_as_mapping;
if (m && m->mp_subscript)
return m->mp_subscript(o, key);
...
В случае массивов NumPy правильная функция находится в mp_subscript
в структуре PyMappingMethods
.
Обратите внимание на дополнительные вызовы функций, прежде чем можно будет вызвать правильную функцию "получить". Эти вызовы добавляют накладные расходы для b[100]
, хотя сколько будет зависеть от того, как был скомпилирован Python/NumPy, системной архитектуры и т.д.
Получение из списка Python
Выше было видно, что вызывается функция PyList_GET_ITEM
. Это короткая функция, которая по существу выглядит так: *:
PyList_GetItem(PyObject *op, Py_ssize_t i)
{
if (!PyList_Check(op)) { // check if list
PyErr_BadInternalCall();
return NULL;
}
if (i < 0 || i >= Py_SIZE(op)) { // check i is in range
if (indexerr == NULL) {
indexerr = PyUnicode_FromString(
"list index out of range");
if (indexerr == NULL)
return NULL;
}
PyErr_SetObject(PyExc_IndexError, indexerr);
return NULL;
}
return ((PyListObject *)op) -> ob_item[i]; // return reference to object
}
* PyList_GET_ITEM
- фактически макроформа этой функции, которая делает то же самое, минус проверку ошибок.
Это означает, что получение элемента в индексе i
в списке Python относительно просто. Внутренне Python проверяет, является ли тип элемента таким, является ли i
в правильном диапазоне для списка, а затем возвращает ссылку на объект в списке.
Получение из массива NumPy
В отличие от этого, NumPy должен выполнять гораздо больше работы, прежде чем можно вернуть значение по запрошенному индексу.
Массивы могут быть проиндексированы различными способами, а NumPy должен решить, какая именно программа индекса необходима. Различные процедуры индексирования обрабатываются в основном кодом mapping.c
.
Все, что используется для индексации массивов NumPy, проходит через функцию prepare_index
, которая начинает разбор индекса и сохраняет информацию о вещании, количество измерений и т.д. Вот сигнатура вызова для функции:
NPY_NO_EXPORT int
prepare_index(PyArrayObject *self, PyObject *index,
npy_index_info *indices,
int *num, int *ndim, int *out_fancy_ndim, int allow_boolean)
/* @param the array being indexed
* @param the index object
* @param index info struct being filled (size of NPY_MAXDIMS * 2 + 1)
* @param number of indices found
* @param dimension of the indexing result
* @param dimension of the fancy/advanced indices part
* @param whether to allow the boolean special case
*/
Функция должна выполнять множество проверок. Даже для относительно простого индекса, такого как b[100,100]
, необходимо сделать много информации, чтобы NumPy мог вернуть ссылку (представление) в правильное значение.
В заключение, для функции "getitem" для NumPy требуется больше времени, и функции, обрабатывающие индексирование массивов, обязательно более сложны, чем единственная функция для списков Python.