Почему генератор, производимый с выходом, быстрее, чем генератор, создаваемый 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 последовательно (немного) медленнее, чем при использовании урожая. Почему это так?

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

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

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

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

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

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

Добавьте в нижнюю часть вашего скрипта следующее:

 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 и с my_sequence и получил довольно последовательные результаты, хотя первый из них работает, по-видимому, немного отстает – возможно, из-за разгонки JIT время или что-то еще):

 0.00285911560059 0.00137305259705 

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

Как я уже упоминал в своем комментарии выше, с вашей генераторной функцией и с 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 
  1. Вы увидите, что я создаю list из каждого, поэтому я обрабатываю последовательности.

  2. Функция генератора почти в 10 раз больше времени для xrange .

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

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