11

Pracuję zgodnie z teorią, że wyrażeń generatora wydają się być bardziej wydajne niż normalne pętle. Ale wtedy natknąłem się na następujący przykład: napisać funkcję, która podała numer, N, a niektóre czynniki, ps, zwraca sumę wszystkich liczb pod N, które są wielokrotnością co najmniej jednego czynnika.Dlaczego ta funkcja wyrażeń generatora jest wolniejsza od wersji pętli?

Oto wersja pętli i krótszą wersję wyrażenie generator:

def loops(N, ps): 
    total_sum = 0 
    for i in xrange(N): 
     for p in ps: 
      if i%p == 0: 
       total_sum += i 
       break 
    return total_sum 

def genexp(N, ps): 
    return sum(i for i in xrange(N) 
       if any(i%p == 0 for p in ps)) 

będę oczekiwać na dwa wykonać mniej więcej równe, z może wersji zrozumieniem trochę szybciej, ale to, co nie spodziewałem było to:

for func in ('loops', 'genexp'): 
    print func, timeit.timeit('%s(100000, [3,5,7])' % func, 
           number=100, 
           setup='from __main__ import %s' % func) 


loops 2.82878184319 
genexp 10.1663100719 

4x wolniej nie jest nawet blisko! Czemu? Co ja nie rozumiem?

+0

Masz * wyrażeń generatorowych *, a nie wyrażeń list. –

+0

@MartijnPieters Dzięki! Najwyraźniej nie jestem facetem z Pythona :) – Barry

Odpowiedz

11

Po pierwsze: wyrażeń generatora są pamięć wydajna, niekoniecznie prędkość skuteczna.

Twój kompaktowy genexp() wersja jest wolniejsza od dwóch powodów:

  • Generator wyrażeń są realizowane za pomocą nowego zakresu (jak nowej funkcji). Produkujesz N nowych zakresów dla każdego testu any(). Tworzenie nowego zakresu i jego niszczenie jest stosunkowo kosztowne, na pewno po wykonaniu w pętli, a następnie w porównaniu z kodem, który tego nie robi.

  • Nazwy sum() i any() to dodatkowe globale, które należy obejrzeć. W przypadku any(), jest to dodatkowe globalne wyszukiwanie w jednym teście. Globale muszą być wyszukiwane w słowniku, w porównaniu z miejscowymi, które są wyszukiwane według indeksu w tablicy C (która jest bardzo szybka).

Ta ostatnia jest tylko niewielkim elementem, większość kosztów leży w tworzeniu i niszczeniu ramek (zakresów); jeśli stworzyć wersję gdzie _any i _sum są mieszkańców do funkcji dostajesz, ale niewielką poprawę wydajności:

>>> def genexp_locals(N, ps, _any=any, _sum=sum): 
...  return _sum(i for i in xrange(N) 
...    if _any(i%p == 0 for p in ps)) 
... 
>>> for func in ('loops', 'genexp', 'genexp_locals'): 
...  print func, timeit.timeit('%s(100000, [3,5,7])' % func, 
...        number=100, 
...        setup='from __main__ import %s' % func) 
... 
loops 2.00835800171 
genexp 6.45241594315 
genexp_locals 6.23843789101 

nie tworzyć lokalny dla xrange zachować ten aspekt samo. Technicznie rzecz biorąc, nazwa _any jest postrzegana jako zamknięcie, a nie lokalnie, przez obiekt kodu wyrażenia generatora, które nie są tak powolne jak wyszukiwania globalne, ale nie tak szybkie, jak wyszukiwanie lokalne.

Powiązane problemy