Почему этот код нарезки быстрее, чем более процедурный код?
У меня есть функция Python, которая берет список и возвращает генератор, дающий 2-кортежи каждой смежной пары, например
>>> list(pairs([1, 2, 3, 4]))
[(1, 2), (2, 3), (3, 4)]
Я рассмотрел реализацию с использованием 2 срезов:
def pairs(xs):
for p in zip(xs[:-1], xs[1:]):
yield p
и один написан в более процедурном стиле:
def pairs(xs):
last = object()
dummy = last
for x in xs:
if last is not dummy:
yield last,x
last = x
Тестирование с использованием range(2 ** 15)
в качестве ввода дает следующие моменты (вы можете найти мой тестовый код и вывести здесь):
2 slices: 100 loops, best of 3: 4.23 msec per loop
0 slices: 100 loops, best of 3: 5.68 msec per loop
Частью хита производительности для sliceless-реализации является сравнение в цикле (if last is not dummy
). Удаление этого (неверный вывод) улучшает его производительность, но оно все же медленнее, чем реализация zip-a-pair-of-slices:
2 slices: 100 loops, best of 3: 4.48 msec per loop
0 slices: 100 loops, best of 3: 5.2 msec per loop
Итак, я в тупике. Зачем сжимать два среза, эффективно повторяя их по списку дважды параллельно, быстрее, чем повторять один раз, обновляя last
и x
, когда вы идете?
ИЗМЕНИТЬ
Дан Ленски предложил третью реализацию:
def pairs(xs):
for ii in range(1,len(xs)):
yield xs[ii-1], xs[ii]
Здесь его сравнение с другими реализациями:
2 slices: 100 loops, best of 3: 4.37 msec per loop
0 slices: 100 loops, best of 3: 5.61 msec per loop
Lenski's: 100 loops, best of 3: 6.43 msec per loop
Это еще медленнее! Что меня озадачивает.
ИЗМЕНИТЬ 2:
ssm предложил, используя itertools.izip
вместо zip
, и он даже быстрее, чем zip
:
2 slices, izip: 100 loops, best of 3: 3.68 msec per loop
Итак, izip
- победитель! Но по-прежнему трудно проверить причины.
Ответы
Ответ 1
Много интересной дискуссии в другом месте этой темы. В принципе, мы начали сравнивать две версии этой функции, которые я буду описывать со следующими немыми именами:
-
Версия zip
-py:
def pairs(xs):
for p in zip(xs[:-1], xs[1:]):
yield p
-
Версия "loopy":
def pairs(xs):
last = object()
dummy = last
for x in xs:
if last is not dummy:
yield last,x
last = x
Так почему же петлевая версия оказывается медленнее? В принципе, я думаю, что это сводится к нескольким вещам:
-
Строчная версия явно выполняет дополнительную работу: она сравнивает идентификаторы двух объектов (if last is not dummy: ...
) на каждой инициализирующей пару итерации внутреннего цикла.
- @mambocab edit показывает, что не делать этого сравнения делает петлевую версию
немного быстрее, но не полностью закрывает пробел.
-
В zippy-версии больше всего содержится в компилированном C-коде, который имеет петлевая версия в коде Python:
-
Объединение двух объектов в tuple
. Завершающая версия имеет yield last,x
, тогда как в zippy-версии кортеж p
поступает прямо от zip
, поэтому он просто делает yield p
.
-
Связывание имен переменных с объектом: циклическая версия делает это дважды в каждом цикле, назначая x
в цикле for
и last=x
. Версия zippy делает это только один раз, в цикле for
.
-
Интересно, что существует один способ, с помощью которого zippy-версия действительно выполняет больше: она использует два listiterator
s, iter(xs[:-1])
и iter(xs[1:])
, которые передаются в zip
. В циклической версии используется один listiterator
(for x in xs
).
- Создание объекта
listiterator
(вывод iter([])
), вероятно, очень оптимизирован, поскольку программисты Python используют его так часто.
- Итерация над списками списков
xs[:-1]
и xs[1:]
- очень легкая операция, которая практически не увеличивает накладные расходы по сравнению с итерированием по всему списку. По сути, это просто означает перемещение начальной или конечной точки итератора, но не изменение того, что происходит на каждой итерации.
Ответ 2
Это я результат для iZip
, который на самом деле ближе к вашей реализации. Похоже на то, чего вы ожидаете. Версия zip
создает весь список в памяти внутри функции, поэтому она самая быстрая. Версия цикла просто проиграет список, так что он немного медленнее. iZip
является самым близким по отношению к коду, но я предполагаю, что есть некоторые бэкэнд-процессы управления памятью, которые увеличивают время выполнения.
In [11]: %timeit pairsLoop([1,2,3,4,5])
1000000 loops, best of 3: 651 ns per loop
In [12]: %timeit pairsZip([1,2,3,4,5])
1000000 loops, best of 3: 637 ns per loop
In [13]: %timeit pairsIzip([1,2,3,4,5])
1000000 loops, best of 3: 655 ns per loop
Ниже приведена версия кода ниже:
from itertools import izip
def pairsIzip(xs):
for p in izip(xs[:-1], xs[1:]):
yield p
def pairsZip(xs):
for p in zip(xs[:-1], xs[1:]):
yield p
def pairsLoop(xs):
last = object()
dummy = last
for x in xs:
if last is not dummy:
yield last,x
last = x
Ответ 3
Я подозреваю, что основная причина, по которой вторая версия медленнее, заключается в том, что она выполняет операцию сравнения для каждой пары, которая yield
s:
# pair-generating loop
for x in xs:
if last is not dummy:
yield last,x
last = x
Первая версия не делает ничего, кроме плюсов. Переименованные переменные цикла эквивалентны этому:
# pair-generating loop
for last,x in zip(xs[:-1], xs[1:]):
yield last,x
Это не особенно красиво или Pythonic, но вы можете написать процедурный вариант без сравнения во внутреннем цикле. Как быстро это выполняется?
def pairs(xs):
for ii in range(1,len(xs)):
yield xs[ii-1], xs[ii]