Почему массивы Python медленные?

153

Я ожидал, array.arrayчто будет быстрее, чем списки, так как массивы кажутся распакованными.

Однако я получаю следующий результат:

In [1]: import array

In [2]: L = list(range(100000000))

In [3]: A = array.array('l', range(100000000))

In [4]: %timeit sum(L)
1 loop, best of 3: 667 ms per loop

In [5]: %timeit sum(A)
1 loop, best of 3: 1.41 s per loop

In [6]: %timeit sum(L)
1 loop, best of 3: 627 ms per loop

In [7]: %timeit sum(A)
1 loop, best of 3: 1.39 s per loop

Что может быть причиной такой разницы?

Валентин Лоренц
источник
4
NumPy инструменты могут эффективно использовать ваш массив:% timeit np.sum (A): 100 циклов, лучшее из 3: 8,87 мс на цикл
BM
6
Я никогда не сталкивался с ситуацией, когда мне нужно было использовать arrayпакет. Если вы хотите сделать значительный объем математики, Numpy работает со скоростью света (то есть C), и обычно лучше, чем наивные реализации таких вещей, как sum()).
Ник Т
40
Близкие избиратели: почему именно это основано на мнении? Похоже, что ОП задает конкретный технический вопрос об измеримом и повторяемом явлении.
Кевин
5
@NickT Читайте анекдот по оптимизации . Оказывается, arrayдовольно быстро конвертировать строку целых чисел (представляющих байты ASCII) в strобъект. Сам Гвидо придумал это только после множества других решений и был весьма удивлен производительностью. Во всяком случае, это единственное место, где я помню, что это полезно. numpyгораздо лучше работать с массивами, но это сторонняя зависимость.
Бакуриу

Ответы:

220

Хранения является «распакованный», но каждый раз , когда вы получаете доступ к элементу Python должен «поле» он (вставить его в обычный объект Python) для того , чтобы сделать что - нибудь с ним. Например, вы sum(A)перебираете массив и упаковываете каждое целое число, по одному, в обычный intобъект Python . Это стоит времени. По вашему sum(L), весь бокс был сделан во время создания списка.

Итак, в конце концов, массив, как правило, медленнее, но требует значительно меньше памяти.


Вот соответствующий код из недавней версии Python 3, но те же основные идеи применимы ко всем реализациям CPython, так как Python был впервые выпущен.

Вот код для доступа к элементу списка:

PyObject *
PyList_GetItem(PyObject *op, Py_ssize_t i)
{
    /* error checking omitted */
    return ((PyListObject *)op) -> ob_item[i];
}

Это очень мало: somelist[i]просто возвращает iый объект в списке (и все объекты Python в CPython являются указателями на структуру, начальный сегмент которой соответствует макету a struct PyObject).

И вот __getitem__реализация для arrayкода с типом l:

static PyObject *
l_getitem(arrayobject *ap, Py_ssize_t i)
{
    return PyLong_FromLong(((long *)ap->ob_item)[i]);
}

Необработанная память обрабатывается как вектор C longцелочисленных от платформы целых чисел; iC longчитается до; и затем PyLong_FromLong()вызывается для переноса («коробки») натива C longв longобъекте Python (который в Python 3, который устраняет различие между Python 2 и intand long, фактически отображается как тип int).

Этот бокс должен выделить новую память для intобъекта Python и добавить C longв него биты натива. В контексте исходного примера время жизни этого объекта очень короткое (достаточно длинное, sum()чтобы добавить содержимое в промежуточный итог), и затем требуется больше времени для освобождения нового intобъекта.

Отсюда и разница в скорости, и всегда, и всегда в реализации CPython.

Тим Питерс
источник
87

Чтобы добавить к отличному ответу Тима Питерса, массивы реализуют протокол буфера , а списки - нет. Это означает, что если вы пишете расширение C (или моральный эквивалент, такой как написание модуля Cython ), то вы можете обращаться к элементам массива и работать с ними гораздо быстрее, чем все, что может сделать Python. Это даст вам значительное улучшение скорости, возможно, на порядок выше. Однако у него есть ряд недостатков:

  1. Теперь вы пишете C вместо Python. Cython - один из способов улучшить это, но он не устраняет многих фундаментальных различий между языками; Вы должны быть знакомы с семантикой C и понимать, что она делает.
  2. PyPy C API работает в некоторой степени , но не очень быстро. Если вы ориентируетесь на PyPy, вам, вероятно, следует просто написать простой код с обычными списками, а затем позволить JITter оптимизировать его для вас.
  3. Расширения C сложнее распространять, чем чистый код Python, потому что они должны быть скомпилированы. Компиляция, как правило, зависит от архитектуры и операционной системы, поэтому вам необходимо убедиться, что вы компилируете для своей целевой платформы.

В зависимости от вашего варианта использования, вы можете использовать кувалду, чтобы ударить муху. Сначала вы должны исследовать NumPy и посмотреть, достаточно ли он силен, чтобы делать то, что вы пытаетесь сделать. Это также будет намного быстрее, чем родной Python, если используется правильно.

Kevin
источник
10

Тим Питерс ответил, почему это медленно, но давайте посмотрим, как это улучшить .

Придерживаясь вашего примера sum(range(...))(в 10 раз меньше, чем ваш пример, чтобы вписаться в память здесь):

import numpy
import array
L = list(range(10**7))
A = array.array('l', L)
N = numpy.array(L)

%timeit sum(L)
10 loops, best of 3: 101 ms per loop

%timeit sum(A)
1 loop, best of 3: 237 ms per loop

%timeit sum(N)
1 loop, best of 3: 743 ms per loop

Таким образом, numpy также нужно блокировать / распаковывать, что имеет дополнительные накладные расходы. Чтобы сделать это быстро, нужно оставаться в коде NumPy C:

%timeit N.sum()
100 loops, best of 3: 6.27 ms per loop

Таким образом, из списка решений для NumPy версии это фактор 16 во время выполнения.

Давайте также проверим, сколько времени занимает создание этих структур данных

%timeit list(range(10**7))
1 loop, best of 3: 283 ms per loop

%timeit array.array('l', range(10**7))
1 loop, best of 3: 884 ms per loop

%timeit numpy.array(range(10**7))
1 loop, best of 3: 1.49 s per loop

%timeit numpy.arange(10**7)
10 loops, best of 3: 21.7 ms per loop

Чистый победитель: Numpy

Также обратите внимание, что создание структуры данных занимает примерно столько же времени, сколько и суммирование, если не больше. Выделение памяти происходит медленно.

Использование памяти тех:

sys.getsizeof(L)
90000112
sys.getsizeof(A)
81940352
sys.getsizeof(N)
80000096

Таким образом, они занимают 8 байтов на число с различными издержками. Для диапазона, который мы используем, достаточно 32-битных, поэтому мы можем сохранить некоторую память.

N=numpy.arange(10**7, dtype=numpy.int32)

sys.getsizeof(N)
40000096

%timeit N.sum()
100 loops, best of 3: 8.35 ms per loop

Но оказывается, что добавление 64-битных целых на моей машине быстрее, чем 32-битных, поэтому это того стоит, только если вы ограничены памятью / пропускной способностью.

Робин Рот
источник
-1

пожалуйста, обратите внимание, что 100000000равно 10^8не 10^7, и мои результаты, как следующие:

100000000 == 10**8

# my test results on a Linux virtual machine:
#<L = list(range(100000000))> Time: 0:00:03.263585
#<A = array.array('l', range(100000000))> Time: 0:00:16.728709
#<L = list(range(10**8))> Time: 0:00:03.119379
#<A = array.array('l', range(10**8))> Time: 0:00:18.042187
#<A = array.array('l', L)> Time: 0:00:07.524478
#<sum(L)> Time: 0:00:01.640671
#<np.sum(L)> Time: 0:00:20.762153
С. Черагифар
источник