Почему генератор, производимый с выходом, быстрее, чем генератор, создаваемый xrange?

Я изучал генераторы Python и решил провести небольшой эксперимент.

TOTAL = 100000000
def my_sequence():
    i = 0
    while i < TOTAL:
        yield i
        i += 1

def my_list():
    return range(TOTAL)

def my_xrange():
    return xrange(TOTAL)    

Использование памяти (с использованием psutil для получения информации о памяти процесса) и время, затраченное на использование time.time()), приведены ниже, после запуска каждого метода несколько раз и принятия среднего значения:

sequence_of_values = my_sequence() # Memory usage: 6782976B  Time taken: 9.53674e-07 s

sequence_of_values2 = my_xrange() # Memory usage: 6774784B  Time taken: 2.14576e-06 s

list_of_values = my_list() # Memory usage: 3266207744B  Time taken: 1.80253s

Я заметил, что генератор с использованием xrange последовательно (немного) медленнее, чем при использовании урожая. Почему это так?

Ответ 1

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

Хорошо, теперь отказ от ответственности...

Первое, что вам нужно заметить, это то, что вы только устанавливаете время для создания объекта generator/xrange. Вы НЕ подсчитываете, сколько времени требуется для фактической итерации значений 1. Есть несколько причин, почему создание генератора может быть быстрее в некоторых случаях, чем создание объекта xrange...

  • Для случая генератора вы создаете генератор - никакой код в генераторе не запускается. Это примерно 1 вызов функции.
  • Для случая xrange вы вызываете эту функцию, а затем вы должны искать глобальное имя xrange, глобальное TOTAL, а затем вам нужно вызвать это встроенное. выполненных в этом случае.

Как для памяти. В обоих ленивых подходах в используемой памяти будет доминировать время выполнения python - не по размеру ваших объектов-генераторов. Единственный случай, когда использование памяти заметно влияет на ваш script, - это случай, когда вы создаете список из 100 миллионов элементов.

Также обратите внимание, что я не могу навсегда подтвердить ваши результаты в моей системе... Используя timeit, я действительно получаю, что my_xrange иногда 2 быстрее построить (на ~ 30 %).

Добавьте в нижнюю часть script следующее:

from timeit import timeit
print timeit('my_xrange()', setup='from __main__ import my_xrange')
print timeit('my_sequence()', setup='from __main__ import my_sequence')

И мои результаты (для CPython на OS-X El-Capitan):

0.227491140366
0.356791973114

Однако pypy, по-видимому, благоприятствует построению генератора (я сначала попробовал его с my_xrange first и my_sequence и получил довольно согласованные результаты, хотя первый для запуска, похоже, был немного отстающим - Возможно, из-за времени прогрева JIT или чего-то еще):

0.00285911560059
0.00137305259705

1 Здесь я бы ожидал, что xrange будет иметь ребро, но опять же, ничего не будет истинным до тех пор, пока вы timeit, а затем это будет только true, если разница в таймингах значительна и это верно только на компьютере, где вы выполняли тайминги.
2 См. главу об отказе от ответственности: -P

Ответ 2

Как я уже упоминал в своем комментарии выше, с вашей функцией генератора и с помощью xrange, вы фактически не создаете последовательность, просто создавая объект. Ответ @mgilson охватывает вызовы, связанные с их созданием.

Что касается собственно чего-то с ними:

>>> TOTAL = 100000
>>> # your functions here
...
>>> import timeit
>>> timeit.timeit("list(my_seq())", setup="from __main__ import my_seq", number=1000)
9.783777457339898
>>> timeit.timeit("list(my_xrange())", setup="from __main__ import my_xrange", number=1000)
1.2652621698083024
>>> timeit.timeit("list(my_list())", setup="from __main__ import my_list", number=1000)
2.666709824464867
>>> timeit.timeit("my_list()", setup="from __main__ import my_list", number=1000)
1.2324339537661615
  • Вы увидите, что я создаю list из каждого, поэтому я обрабатываю последовательности.

  • Функция генератора почти в 10 раз превышает время xrange.

  • list(my_list) является избыточным, так как my_list уже возвращает список, созданный range, поэтому я сделал это еще раз без вызова list().

  • range почти совпадает с xrange, но это потому, что я уменьшил TOTAL. Самое большое различие в том, что range будет потреблять больше памяти, поскольку сначала создает весь список, и поэтому занимает больше времени только в этой части. Создание списка из xrange = range, эффективно. Таким образом, последняя используемая память была бы такой же, и поскольку я просто создаю список из xrange, трудно увидеть разницу в этом тривиальном случае.