Почему для объединения массивов numpy, чем списков, требуется больше времени? [Дубликат]

Я расскажу вам о секрете: лучший способ использовать timeit находится в командной строке.

В командной строке timeit выполняет правильный статистический анализ: он сообщает вам как долго длился самый короткий пробег. Это хорошо, потому что ошибка во времени положительна. Таким образом, самое короткое время имеет наименьшую ошибку. Невозможно получить отрицательную ошибку, потому что компьютер никогда не может вычислить быстрее, чем он может вычислить!

Итак, интерфейс командной строки:

%~> python -m timeit "1 + 2"
10000000 loops, best of 3: 0.0468 usec per loop

Это довольно просто, eh?

Вы можете настроить файл:

%~> python -m timeit -s "x = range(10000)" "sum(x)"
1000 loops, best of 3: 543 usec per loop

, который тоже полезен!

Если вы хотите несколько строк, вы можете либо использовать оболочку автоматическое продолжение или использование отдельных аргументов:

%~> python -m timeit -s "x = range(10000)" -s "y = range(100)" "sum(x)" "min(y)"
1000 loops, best of 3: 554 usec per loop

Это дает настройку

x = range(1000)
y = range(100)

и времени

sum(x)
min(y)

Если вы хотите чтобы иметь более длинные скрипты, у вас может возникнуть соблазн перейти на timeit внутри скрипта Python. Я предлагаю избегать этого, потому что анализ и синхронизация просто лучше в командной строке. Вместо этого я стараюсь создавать сценарии оболочки:

 SETUP="

 ... # lots of stuff

 "

 echo Minmod arr1
 python -m timeit -s "$SETUP" "Minmod(arr1)"

 echo pure_minmod arr1
 python -m timeit -s "$SETUP" "pure_minmod(arr1)"

 echo better_minmod arr1
 python -m timeit -s "$SETUP" "better_minmod(arr1)"

 ... etc

Это может занять несколько больше времени из-за множественных инициализаций, но обычно это не очень важно.


Но что если вы хотите использовать timeit внутри вашего модуля?

Ну, простой способ сделать:

def function(...):
    ...

timeit.Timer(function).timeit(number=NUMBER)

, и это дает вам кумулятивное ( not минимум!) время для запуска этого количества раз.

Чтобы получить хороший анализ, используйте .repeat и возьмите минимум:

min(timeit.Timer(function).repeat(repeat=REPEATS, number=NUMBER))

Обычно вы должны комбинировать это с functools.partial вместо lambda: ..., чтобы снизить накладные расходы. Таким образом, у вас может быть что-то вроде:

from functools import partial

def to_time(items):
    ...

test_items = [1, 2, 3] * 100
times = timeit.Timer(partial(to_time, test_items)).repeat(3, 1000)

# Divide by the number of repeats
time_taken = min(times) / 1000

Вы также можете сделать:

timeit.timeit("...", setup="from __main__ import ...", number=NUMBER)

, который даст вам что-то ближе к интерфейсу из командной строки, но в гораздо менее прохладной манере. "from __main__ import ..." позволяет вам использовать код из вашего основного модуля внутри искусственной среды, созданной timeit.

Стоит отметить, что это удобная обертка для Timer(...).timeit(...), и поэтому она не особенно хороша в время. Я лично предпочитаю использовать Timer(...).repeat(...), как я показал выше.


Предупреждения

Есть несколько предостережений с timeit, которые находятся повсюду.

  • Накладные расходы не учитываются. Скажите, что хотите время x += 1, чтобы узнать, сколько времени занимает добавление:
    >>> python -m timeit -s "x = 0" "x += 1"
    10000000 loops, best of 3: 0.0476 usec per loop
    
    Ну, это не 0.0476 мкс. Вы только знаете, что это меньше , чем это. Вся ошибка положительная. Поэтому попробуйте найти чистые накладные расходы:
    >>> python -m timeit -s "x = 0" ""      
    100000000 loops, best of 3: 0.014 usec per loop
    
    Это хорошая 30% накладных расходов только с момента! Это может значительно исказить относительные тайминги. Но вы действительно заботились о добавлении таймингов; временные интервалы поиска для x также должны быть включены в накладные расходы:
    >>> python -m timeit -s "x = 0" "x"
    100000000 loops, best of 3: 0.0166 usec per loop
    
    Разница не намного больше, но она есть.
  • Методы мутации опасны.
    >>> python -m timeit -s "x = [0]*100000" "while x: x.pop()"
    10000000 loops, best of 3: 0.0436 usec per loop
    
    Но это совершенно неверно! x - это пустой список после первой итерации. Вам нужно будет повторно инициализировать:
    >>> python -m timeit "x = [0]*100000" "while x: x.pop()"
    100 loops, best of 3: 9.79 msec per loop
    
    Но тогда у вас много накладных расходов. Учтите это отдельно.
    >>> python -m timeit "x = [0]*100000"                   
    1000 loops, best of 3: 261 usec per loop
    
    Обратите внимание, что вычитание служебных данных здесь разумно только потому, что накладные расходы являются малой частью времени. Для вашего примера стоит отметить, что и Sorting Sort и Tim Sort имеют совершенно необычное временное поведение для уже отсортированных списков. Это означает, что вам понадобится random.shuffle между сортировками, если вы хотите избежать разрушения ваших таймингов.
9
задан Wayne Werner 5 February 2016 в 20:59
поделиться

2 ответа

Мы можем немного потрудиться, чтобы понять это:

>>> import numpy as np
>>> a = np.arange(32)
>>> a
array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16,
       17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31])
>>> a.data
<read-write buffer for 0x107d01e40, size 256, offset 0 at 0x107d199b0>
>>> id(a.data)
4433424176
>>> id(a[0])
4424950096
>>> id(a[1])
4424950096
>>> for item in a:
...   print id(item)
... 
4424950096
4424950120
4424950096
4424950120
4424950096
4424950120
4424950096
4424950120
4424950096
4424950120
4424950096
4424950120
4424950096
4424950120
4424950096
4424950120
4424950096
4424950120
4424950096
4424950120
4424950096
4424950120
4424950096
4424950120
4424950096
4424950120
4424950096
4424950120
4424950096
4424950120
4424950096
4424950120

Итак, что здесь происходит? Во-первых, я взглянул на расположение памяти буфера памяти массива. Это на 4433424176. Это само по себе не является также . Тем не менее, numpy хранит его данные как непрерывный массив C, поэтому первый элемент в массиве numpy должен соответствовать адресу памяти самого массива, но это не так:

>>> id(a[0])
4424950096

, и хорошо, что это не так, потому что это сломает инвариант в python, что у 2 объектов никогда не будет одинакового id в течение их жизни.

Итак, как numpy выполняет это? Ну, ответ заключается в том, что numpy должен обернуть возвращаемый объект типом python (например, numpy.float64 или numpy.int64 в этом случае), который требует времени, если вы повторяете item-by-item1. Дальнейшее доказательство этого демонстрируется при итерации. Мы видим, что мы чередуем между двумя отдельными идентификаторами, итерациями по массиву. Это означает, что распределитель памяти и сборщик мусора python работают сверхурочно для создания новых объектов, а затем освобождают их.

Список не имеет накладных расходов на распределитель памяти / сборщика мусора. Объекты в списке уже существуют как объекты python (и они все равно будут существовать после итерации), поэтому не играет никакой роли в итерации по списку.

Методология синхронизации:

Также обратите внимание: ваши тайминги немного отброшены по вашим предположениям. Вы предполагали, что в обоих случаях k + 1 должно занимать примерно такое же количество времени, но это не так. Обратите внимание, если я повторяю ваши тайминги без , делая какое-либо дополнение:

mgilson$ python -m timeit -s "import numpy" "for k in numpy.arange(5000): k"
1000 loops, best of 3: 233 usec per loop
mgilson$ python -m timeit "for k in range(5000): k"
10000 loops, best of 3: 114 usec per loop

, разница в коэффициенте разницы составляет всего 2. Однако выполнение добавления приводит к разнице в 5 раз:

mgilson$ python -m timeit "for k in range(5000): k+1"
10000 loops, best of 3: 179 usec per loop
mgilson$ python -m timeit -s "import numpy" "for k in numpy.arange(5000): k+1"
1000 loops, best of 3: 786 usec per loop

Для удовольствия просто добавьте:

$ python -m timeit -s "v = 1" "v + 1"
10000000 loops, best of 3: 0.0261 usec per loop
mgilson$ python -m timeit -s "import numpy; v = numpy.int64(1)" "v + 1"
10000000 loops, best of 3: 0.121 usec per loop

И, наконец, ваше время также включает время построения списка / массива, которое не является идеальным:

mgilson$ python -m timeit -s "v = range(5000)" "for k in v: k"
10000 loops, best of 3: 80.2 usec per loop
mgilson$ python -m timeit -s "import numpy; v = numpy.arange(5000)" "for k in v: k"
1000 loops, best of 3: 237 usec per loop

Обратите внимание, что в этом случае nump действительно удалился от решения списка. Это показывает, что итерация на самом деле меньше медленнее, и вы можете получить некоторые ускорения, если вы конвертируете типы numpy в стандартные типы python.

1Note, this doesn ' t занимает много времени, когда разрезает, потому что ему нужно только выделить O (1) новые объекты, поскольку numpy возвращает view в исходный массив.

14
ответ дан mgilson 19 August 2018 в 13:00
поделиться
  • 1
    Да, я покупаю, что он, похоже, в основном конвертируется в преобразования данных. Обычно я делаю +1 при тестировании, потому что я не уверен, что k будет оптимизирован. Отмечено ответ – mechsin 5 February 2016 в 22:07
  • 2
    Я продемонстрировал в stackoverflow.com/a/34273109/901925 , что a[0] имеет все свойства array. Были другие вопросы SO, которые спрашивают, что быстрее, список или массив. – hpaulj 5 February 2016 в 22:52
  • 3
    @mechsin - k не будет оптимизирован - но даже если бы это было так, это не было бы большой проблемой, пока итерация не была. Итерация не может оптимизироваться, потому что нет возможности для python знать априори, если итерация по элементу имеет побочные эффекты (например, потребление генератора по сравнению с неиспользуемым итерабельным, например список). – mgilson 5 February 2016 в 23:22

Использование python 2.7

Ниже приведены мои скорости вместе с xrange:

python -m timeit -s "import numpy" "for k in numpy.arange(5000): k+1"

1000 циклов, лучше всего 3: 1,22 мс за цикл

python -m timeit "for k in range(5000): k+1"

10000 циклов, наилучшее из 3: 186 usec за цикл

python -m timeit "for k in xrange(5000): k+1"

10000 циклов, лучше всего 3: 161 usec за цикл

Numpy заметно медленнее, потому что он выполняет итерацию по numpy-specific array. Это не основная функция. Во многих случаях их следует рассматривать скорее как монолитный набор чисел, а не простых списков / итераций. Например, если у нас есть довольно большой и-пиш-список чисел, который мы хотим поднять до третьей степени, мы можем сделать что-то вроде этого:

python -m timeit "lst1 = [x for x in range(100000)];" "lst2 = map(lambda x: x**3, lst1)"

10 петель, лучше всего 3: 125 msec per loop

Примечание: lst1 представляет собой произвольный список. Я знаю, что вы можете ускорить это в исходной лямбда, выполнив x ** 3 для x в диапазоне, но это связано с списком, который должен уже существовать и может быть очень не последовательным.

В любом случае, numpy должен рассматриваться как массив:

python -m timeit -s "import numpy" "lst1 = numpy.arange(100000)" "lst2 = lst1**2"

10000 циклов, лучше всего 3: 120 usec за цикл

Скажем, у вас было два списка произвольных значений , каждый из которых вы хотите размножать вместе. В ванильном python вы можете сделать:

python -m timeit -s "lst1 = [x for x in xrange(0, 10000, 2)]" "lst2 = [x for x in xrange(2, 10002, 2)]" "lst3 = [x*y for x,y in zip(lst1, lst2)]"

1000 циклов, лучше всего 3: 736 usec за цикл

И в Numpy:

python -m timeit -s "import numpy" "lst1 = numpy.arange(0, 10000, 2)" "lst2 = numpy.arange(2, 10002, 2)" "lst3 = lst1*lst2"

100000 циклов, лучше всего 3: 10,9 usec за цикл

В этих последних двух примерах NumPy взлетает вперед в качестве явного победителя. Для простой итерации по списку диапазон или xrange вполне достаточен, но ваш пример не учитывает истинную цель массивов Numpy. Это сравнение самолетов и автомобилей; да, самолеты, как правило, быстрее для того, что они предназначены, но попытка летать в ваш местный супермаркет не является разумной.

0
ответ дан Goodies 19 August 2018 в 13:00
поделиться
  • 1
    Я оптимизировал медленный код с помощью векторизации и массивов Numpy, поэтому я хорошо знаю, что цель намеревается. Я действительно хотел задать вопрос, который я задал, и почему он перебирает два объекта, которые по сути являются такими же разными. Я думаю, что mgilson, вероятно, прав, это связано с преобразованиями данных. – mechsin 5 February 2016 в 22:02
Другие вопросы по тегам:

Похожие вопросы: