Преобразование в то время как генератор в 3,4 раза замедляет работу

Что происходит? Может кто-нибудь объяснить мне, что здесь происходит, я изменился в узкой петле:

##            j=i
##            while j < ls - 1 and len(wordlist[j]) > lc: j+=1
            j = next(j for j in range(i,ls) if len(wordlist[j]) <=  lc)

Прокомментированная while версия запускала всю программу: 625 мс, следующая версия генератора выполняла всю программу во время 2.125 с.

Что может быть причиной того, что эта более питоническая версия вызывает такую ​​катастрофу в производительности?

EDIT: Может быть, это вызвано использованием модуля psyco? Разумеется, по крайней мере, время работы с Python 2.7, которое не было psyco, было 2.141 для следующей версии, означает почти то же самое, что и Python 2.6 с psyco.

После удаления файлов *.pyc я не получил код, чтобы замедлить работу. Затем, когда я удалил импорт psyco из модуля библиотеки, я также получил 2,6 такта и для использования без psyco, результаты для версии без psyco и версии psyco (так как теперь также замедляется и процедура библиотеки), и это также актуально:)

не psyco:

  • пока: подготовка в библиотеке: 532 мс, общее время работы 2.625 с
  • next: подготовка в библиотеке: 532 мс, общее время работы (time.clock()): 2.844 с (версия с xrange тем же временем стены)

Психо:

  • пока: подготовка в библиотеке: 297 мс, общее время работы: 609..675 мс
  • next: подготовка в библиотеке: 297 мс, общее время работы: 1.922 с (версия с диапазоном вместо xrange везде в программе: 1.985 с)

Работа в WindowsXP AMD Sempron 3100+ с 2 ГБ оперативной памяти. Подсчет циклов и вызовов с двумя глобалями:

    j=i
    callcount += 1
    while j < ls - 1 and len(wordlist[j]) > lc:
        j+=1
        loopcount += 1

Результат для тестового ввода с помощью psyco:

Finished in 625 ms
Loopcount: 78317
Callcount: 47970
Ration: 1.633

Итак, цикл находится внутри жесткого цикла, но в среднем выполняется всего пару раз (обратите внимание, что два приращения глобальных счетчиков не замедляли код в psyco)

Выводы: Несмотря на очень чувствительный характер алгоритма относительно длины словаря, который заставлял меня передавать некоторые замечательные слова из рассмотрения этим циклом, позже основные случаи рекурсии проверяются поиском по словарю, который является O (n), поэтому очень выгодная более ранняя оптимизация становится не очень полезной, даже при более длительном вводе и перемещении счетчика вызовов в начале функции, показало, что количество вызовов не зависит от длины словаря, но количество внешних циклов слабо снижено ( код, первоначально опубликованный, находится в элементе if if).

Более длительное время выполнения (29 372 решения) с циклом while и удалением всего цикла (с использованием я вместо j) (подготовка библиотеки 312 мс):

  • Без цикла: количество ветвей elif: 485488, outerloopcount: 10129147, отношение: 0,048, время работы 6000 с (без счетчиков: 4,594 с)
  • С петлей: loopcount: 19355114, outercount: 8194033, соотношение: 0,236, время выполнения 5,704 с (без счетчиков: 4,688 с)

(время работы без цикла, счетчиков и psyco: 32 792 с, библиотека 608 мс)

Таким образом, без дополнительных счетчиков преимущество этого цикла, использующего psyco, в более тяжелом случае: (4688-4594) * 100/4688.0% = 2%

Это вдохновило меня на отменить еще одну более раннюю оптимизацию, о чем я задавался в DaniWeb. Раньше версия кода выполнялась быстрее, когда наименьший размер слова был глобальным, а не параметрирующим. Согласно документации, вызовы с локальной переменной быстрее, но при этом затраты на рекурсию более тяжелые. Теперь в более трудном случае эта другая реверсия оптимизации привела к более ожидаемому результату работы в случае без оптимизации длины слова: время работы с psyco составило 312 мс, 4 469..4 484 с общее время работы. Таким образом, это сделало очиститель кода и принесло больше пользы в этом случае, поскольку удаленный цикл имел. И добавление параметра в версию с циклом while не сильно изменило время выполнения (изменение стало больше для кода подготовки библиотеки)

**What I learned from this: If you do n optimizations for speed 
you must check the first n-1 optimizations after doing nth one**

Ответ 1

Я обнаружил, что использование генераторов часто может быть медленнее, чем генерация всего списка, что немного противоречит интуиции. Мне удалось установить узкие места производительности, просто добавив пару [].

Например, сравните их:

$ python -m timeit -n 1000 "' '.join(c for c in 'hello world')"
1000 loops, best of 3: 6.11 usec per loop
$ python -m timeit -n 1000 "' '.join([c for c in 'hello world'])"
1000 loops, best of 3: 3.79 usec per loop

Это почти в два раза быстрее, чем генерировать весь список, а не использовать генератор даже для такого простого случая!

Изменить: Как говорит Томас Воутерс в комментариях, причина, по которой генератор работает медленнее, заключается в том, что это такой простой случай. Для баланса здесь есть его тест, в котором генератор является явным победителем:

$ python -m timeit -s "s = 'hello world' * 10000" -s "class C: pass" "for i in (C() for c in s): pass"
10 loops, best of 3: 33.6 msec per loop
$ python -m timeit -s "s = 'hello world' * 10000" -s "class C: pass" "for i in [C() for c in s]: pass"
10 loops, best of 3: 172 msec per loop

Ответ 2

Эти два не эквивалентны.

j=i
while j < ls - 1 and len(wordlist[j]) > lc: 
    j+=1

остановит цикл while, как только список слов [j] <= lc. Вероятно, он мог пройти через нулевые моменты цикла, если первое слово в списке короче или равно lc.

j = next(j for j in range(i,ls) if len(wordlist[j]) <=  lc)

будет продолжать итерирование по всему диапазону я до ls, независимо от длины слов в списке.

Изменить: игнорировать выше - как указал Амбер, вызов next() означает, что выражение генератора оценивается только до тех пор, пока не будет возвращен первый результат. В этом случае я подозреваю, что разница во времени происходит от использования range() вместо xrange() (если это не Python 3.x). В Python 2.x range() создаст полный список в памяти, даже если выражение генератора возвращает только первое значение.