2012-01-08 20 views
5

Podczas profilowania mojej aplikacji Pythona, odkryłem, że len() wydaje się być bardzo drogie, jeden przy użyciu zestawów. Zobacz poniższy kod:Profilowane wydajność len (zestaw) vs. zestawu .__ len __() w Pythonie 3

import cProfile 

def lenA(s): 
    for i in range(1000000): 
     len(s); 

def lenB(s): 
    for i in range(1000000): 
     s.__len__(); 

def main(): 
    s = set(); 
    lenA(s); 
    lenB(s); 

if __name__ == "__main__": 
    cProfile.run("main()","stats"); 

Według statystyk Profiler za Poniżej lenA() wydaje się 14 razy wolniej niż lenB():

ncalls tottime percall cumtime percall filename:lineno(function) 
     1 1.986 1.986 3.830 3.830 .../lentest.py:5(lenA) 
1000000 1.845 0.000 1.845 0.000 {built-in method len} 
     1 0.273 0.273 0.273 0.273 .../lentest.py:9(lenB) 

Am I czegoś brakuje? Obecnie używam __len__() zamiast len(), ale kod wygląda brudna :(

+7

Dlaczego używasz 'cProfile' zamiast' timeit'? Ta pierwsza służy do znajdowania wąskich gardeł w dużych programach i poświęca pewną dokładność na małą skalę. Ta ostatnia służy względnie precyzyjnemu pomiarowi ogólnej wydajności małych fragmentów. 'timeit' powinno być pierwszym wyborem dla takich mikro-znaków. A dla mnie to oznacza mniej ekstremalną różnicę (0.0879 mikrosekundy na 'len' rozmowy, 0,158 mikrosekundy na' .__ rozmowy len__' => 'len' jest 70% wolniej). – delnan

+0

Dzięki @delnan, jestem całkiem nowy w Pythonie. Używając 'timeit', uzyskuję również podobny stosunek. Rzeczywiście, mój program jest znacznie większy niż powyższy kod, ale zaskoczyło mnie, że funkcja 'len()' pojawiła się jako jedno z głównych wąskich gardeł. OK, więc zignoruję 'len()' i skupię się na moich własnych funkcjach, prawda? – Tregoreg

Odpowiedz

13

Oczywiście len ma pewien narzut, ponieważ robi wywołanie funkcji i przekłada AttributeError do TypeError. Również set.__len__ jest taka prosta operacja, która to wiąże się z być bardzo szybko w porównaniu do czegokolwiek, ale nadal nie znaleźć czegoś podobnego różnicy 14x przy użyciu timeit:

In [1]: s = set() 

In [2]: %timeit s.__len__() 
1000000 loops, best of 3: 197 ns per loop 

In [3]: %timeit len(s) 
10000000 loops, best of 3: 130 ns per loop 

należy zawsze po prostu zadzwonić len, nie __len__ Jeśli wywołanie len jest. szyjka w swoim programie powinieneś przemyśleć jego projekt, np. pamięci podręcznej lub obliczenia ich bez wywoływania len.

+0

+1: W szczególności nie należy optymalizować przedwcześnie. Testy porównawcze mogą być wadliwe i, jak zapewne zauważyliście, trzy benchmarki prawdopodobnie zwrócą trzy różne wyniki; i może się okazać, że porównujesz coś zupełnie innego, niż się spodziewałeś dzięki takiemu mikro-benchmarkowi. Obraźliwe, że 'len' nie może być szybszy, jak nazywa' __len__'. Ale to wszystko, co jest pewne. –

+2

@ Anony-Mousse: faktycznie, po prostu spojrzałam na moje własne wyniki i dopiero teraz zobaczyłam, że 'len' jest szybszy niż' __len__'. Nie jestem pewien, jak do tego doszło. –

+2

's .__ len__' również wywołuje funkcję, * i * musi wyszukać atrybut. To przeważa nad globalnym wyszukiwaniem 'len'. – WolframH

1

To miał być komentarz, ale po komentarzu larsman na jego kontrowersyjnymi wynikami a wynikami dostałam, myślę, że warto dodać swoje dane do wątku.

Próbując mniej więcej taka sama konfiguracja Mam przeciwnie PO dostał, i w tym samym kierunku skomentowane przez larsman:

12.1964105975 <- __len__ 
6.22144670823 <- len() 

C:\Python26\programas> 

Test:

def lenA(s): 
    for i in range(100): 
     len(s); 

def lenB(s): 
    for i in range(100): 
     s.__len__(); 

s = set() 

if __name__ == "__main__": 

    from timeit import timeit 
    print timeit("lenB(s)", setup="from __main__ import lenB, s") 
    print timeit("lenA(s)", setup="from __main__ import lenA, s") 

To ActivePython 2,6. 7 64bit w win7

3

jest to ciekawe spostrzeżenie o profilera, który nie ma nic wspólnego z rzeczywistym wykonywaniem funkcji len. Widzisz, w statystykach Profiler, istnieją dwie linie dotyczące lenA:

ncalls tottime percall cumtime percall filename:lineno(function) 
     1 1.986 1.986 3.830 3.830 .../lentest.py:5(lenA) 
1000000 1.845 0.000 1.845 0.000 {built-in method len} 

... podczas gdy istnieje tylko jeden wiersz dotyczący lenB:

 1 0.273 0.273 0.273 0.273 .../lentest.py:9(lenB) 

Profiler Upłynął czas każdego pojedynczego połączenia z lenA do len, ale jako całość zmienił się na lenB. Czas wywołania zawsze dodaje trochę narzut; w przypadku lenA widzisz, że ten narzut zwielokrotniony milion razy.

+1

Myślę, że twój punkt jest absolutnie precyzyjny. Chodzi o "cProfile" narzut, a nie o wydajność funkcji 'len'. – Tregoreg

Powiązane problemy