2013-03-19 8 views
5

Obliczam odległość euklidesową między dwoma wektorami reprezentowanymi przez krotki.Czy proste obliczenia na zmiennej długości mogą być wykonywane szybciej w Pythonie?

(u[0]-v[0])**2 + (u[1]-v[1])**2 + (u[3]-v[3])**2 ... 

Zakodowany sposób działania jest dość szybki. Nie chciałbym jednak przyjąć żadnych założeń dotyczących długości tych wektorów. Wynika to w roztworach, takich jak:

sum([(a-b)**2 for a, b in izip(u, v)]) # Faster without generator 

lub

sum = 0 
for i in xrange(len(u)): 
    sum += (u[i]-v[i])**2 

które obracają się znacznie (co najmniej dwukrotnie) wolniej niż w pierwszej wersji. Czy jest jakiś sprytny sposób na zoptymalizowanie tego, bez uciekania się do NumPy/SciPy? Mam świadomość, że te pakiety oferują najszybszy sposób robienia takich rzeczy, ale w tej chwili bardziej próbuję zdobyć doświadczenie w optymalizowaniu "gołego Pythona". Co znalazłem prace szybko jest dynamicznie zbudować ciąg, który definiuje funkcję i exec(), ale to jest naprawdę ostatecznością, powiedziałbym ...

Wymagania:

  • CPython 2,7
  • biblioteka standardowa tylko
  • „Real” (na przykład bez exec()), czysty Python

Nawet jeśli moje pytanie jest o kilku drobnych operacji w ogóle, możesz zakładać w swoim rozwiązaniu, że jeden z wektorów pozostaje taki sam w ciągu kilku wywołań funkcji.

+2

Masz tylko do [timeit] (http://docs.python.org/2/library/timeit.html). W rozwiązaniu nr 2 tworzysz listę, której nie potrzebujesz. Spróbuj 'sum ((a-b) ** 2 dla a, b in izip (u, v))'. W rozwiązaniu # 3 indeksujesz zbyt często. Spróbuj 'dla a, b w izip (u, v): sum + = (a-b) ** 2' –

+0

Skomentował, że jest szybszy jako lista niż generator. Wygląda na to, że wie, co robi. – FogleBird

+0

@Rob Rzeczywiście zrobiłem dużo czasu i nie mogłem wymyślić rozwiązania, które zbliżyłoby się do zakodowanego wariantu. Dlatego wysłałem tutaj moje pytanie. Jak już zauważyłem, # 2 jest szybsze z tą listą niż bez (ponieważ generatory dają pewien narzut, a same wektory nie są duże). –

Odpowiedz

1

To, co rozumiem, to to, że tak naprawdę nie musisz robić kodu szybciej, po prostu chcesz wiedzieć, dlaczego jest wolniejszy. Aby odpowiedzieć na to pytanie, spójrzmy na demontaż. Na potrzeby tej dyskusji zamierzam zawijać każdą metodę w wywołaniu funkcji, ładowanie u i v, a polecenie powrotu może być ignorowane w każdym demontażu.

def test1(u, v): 
    return (u[0]-v[0])**2 + (u[1]-v[1])**2 + (u[3]-v[3])**2 

dis.dis(test1) 
0 LOAD_FAST    0 (u) 
3 LOAD_CONST    1 (0) 
6 BINARY_SUBSCR  
7 LOAD_FAST    1 (v) 
10 LOAD_CONST    1 (0) 
13 BINARY_SUBSCR  
14 BINARY_SUBTRACT  
15 LOAD_CONST    2 (2) 
18 BINARY_POWER   
19 LOAD_FAST    0 (u) 
22 LOAD_CONST    3 (1) 
25 BINARY_SUBSCR  
26 LOAD_FAST    1 (v) 
29 LOAD_CONST    3 (1) 
32 BINARY_SUBSCR  
33 BINARY_SUBTRACT  
34 LOAD_CONST    2 (2) 
37 BINARY_POWER   
38 BINARY_ADD   
39 LOAD_FAST    0 (u) 
42 LOAD_CONST    4 (3) 
45 BINARY_SUBSCR  
46 LOAD_FAST    1 (v) 
49 LOAD_CONST    4 (3) 
52 BINARY_SUBSCR  
53 BINARY_SUBTRACT  
54 LOAD_CONST    2 (2) 
57 BINARY_POWER   
58 BINARY_ADD   
59 RETURN_VALUE   

Pierwszy przykład obcięłam na długość 3, ponieważ powtarzałbym ten sam wzór w kółko. Możesz szybko zobaczyć, że nie ma żadnego wezwania do działania funkcji i prawie tłumacz wykonuje minimalną możliwą pracę na tych operandach, aby osiągnąć twój wynik.

def test2(u, v): 
    sum((a-b)**2 for a, b in izip(u, v)) 

dis.dis(test2) 
0 LOAD_GLOBAL    0 (sum) 
3 LOAD_CONST    1 (<code object <genexpr> at 02C6F458, file "<pyshell#10>", line 2>) 
6 MAKE_FUNCTION   0 
9 LOAD_GLOBAL    1 (izip) 
12 LOAD_FAST    0 (u) 
15 LOAD_FAST    1 (v) 
18 CALL_FUNCTION   2 
21 GET_ITER    
22 CALL_FUNCTION   1 
25 CALL_FUNCTION   1 
28 RETURN_VALUE   

co widzimy jest to, że możemy utworzyć funkcję z wyrażeniem generatora, obciążenia 2 globalne (suma i iZIP globalne wyszukiwań są wolniejsze niż lokalnych wyszukiwań, nie możemy uniknąć ich raz, ale jeśli zostanie wywołany w ciasnej pętli, wiele osób przypisuje je do lokalnego, takiego jak _izip lub _sum), a następnie cierpi 4 drogie operacje w bajtode z rzędu, wywołując izip, pobierając iterator z generatora, wywołując funkcję utworzone przez generator, a następnie wywołanie funkcji sumy (która pochłonie iterator i doda każdy element przed powrotem).

def test3(u, v): 
    sum = 0 
    for i in xrange(len(u)): 
     sum += (u[i]-v[i])**2 

dis.dis(test3) 
0 LOAD_CONST    1 (0) 
3 STORE_FAST    2 (sum) 

6 SETUP_LOOP    52 (to 61) 
9 LOAD_GLOBAL    0 (xrange) 
12 LOAD_GLOBAL    1 (len) 
15 LOAD_FAST    0 (u) 
18 CALL_FUNCTION   1 
21 CALL_FUNCTION   1 
24 GET_ITER    
25 FOR_ITER    32 (to 60) 
28 STORE_FAST    3 (i) 

31 LOAD_FAST    2 (sum) 
34 LOAD_FAST    0 (u) 
37 LOAD_FAST    3 (i) 
40 BINARY_SUBSCR  
41 LOAD_FAST    1 (v) 
44 LOAD_FAST    3 (i) 
47 BINARY_SUBSCR  
48 BINARY_SUBTRACT  
49 LOAD_CONST    2 (2) 
52 BINARY_POWER   
53 INPLACE_ADD   
54 STORE_FAST    2 (sum) 
57 JUMP_ABSOLUTE   25 
60 POP_BLOCK   
61 LOAD_CONST    0 (None) 
64 RETURN_VALUE 

co widzimy tutaj jest bardziej prosta wersja tego, co dzieje się w test2. Bez generowania wyrażeń lub wywołania sumy, ale zastąpiliśmy to wezwanie wywołania funkcji niepotrzebnym wywołaniem funkcji, wykonując xrange(len(u)) zamiast szybszego rozwiązania sugerowanego przez @ Lucas Malor.

def test4(u, v): 
    mysum = 0 
    for a, b in izip(u, v) : 
     mysum += (a-b)**2 
    return mysum 

dis.dis(test4) 
0 LOAD_CONST    1 (0) 
3 STORE_FAST    2 (mysum) 

6 SETUP_LOOP    47 (to 56) 
9 LOAD_GLOBAL    0 (izip) 
12 LOAD_FAST    0 (u) 
15 LOAD_FAST    1 (v) 
18 CALL_FUNCTION   2 
21 GET_ITER    
22 FOR_ITER    30 (to 55) 
25 UNPACK_SEQUENCE   2 
28 STORE_FAST    3 (a) 
31 STORE_FAST    4 (b) 

34 LOAD_FAST    2 (mysum) 
37 LOAD_FAST    3 (a) 
40 LOAD_FAST    4 (b) 
43 BINARY_SUBTRACT  
44 LOAD_CONST    2 (2) 
47 BINARY_POWER   
48 INPLACE_ADD   
49 STORE_FAST    2 (mysum) 
52 JUMP_ABSOLUTE   22 
55 POP_BLOCK   

56 LOAD_FAST    2 (mysum) 
59 RETURN_VALUE 

Powyższe stanowi wkład @Lucas Malor i jest szybszy na kilka sposobów. Zastępuje operacje indeksu dolnego rozpakowywaniem przy jednoczesnym zmniejszeniu liczby wywołań do 1. Jest to w wielu przypadkach tak szybkie, jak osiągniesz dzięki ograniczeniom, które nam dałeś.

Należy pamiętać, że warto byłoby uzyskać tylko eval wygenerowany ciąg w czasie wykonywania, podobny do funkcji w teście 1, jeśli zamierzano wywołać funkcję wystarczająco długo, aby zasłużyć na narzut. Zauważ również, że ponieważ długość u i v staje się coraz większa (co jest typowym sposobem oceny algorytmów tego typu), wezwanie do działania innych rozwiązań staje się coraz mniejsze i dlatego w większości przypadków najbardziej czytelne rozwiązanie jest znacznie lepsze. . W tym samym czasie, chociaż w małych przypadkach jest wolniej, jeśli długość sekwencji, u i v, może być bardzo długa, zalecam wyrażenie generujące, a nie rozumienie listy. Oszczędności w pamięci powodują w większości przypadków znacznie szybsze wykonanie (i szybsze gc).

Ogólnie, moja rekomendacja jest taka, że ​​niewielkie przyspieszenie w przypadku krótkich sekwencji nie jest po prostu warte zwiększenia rozmiaru kodu i niespójnego zachowania z innymi implementacjami python, na które patrzysz, wykonując mikro-optymalizacje. "Najlepsze" rozwiązanie to prawie na pewno test2.

+0

Bardzo dziękuję za twój szeroki wkład w dyskusję i zamknijmy się z tym. Trudno było zwolnić kilka sekund z mojego kodu bez korzystania ze specjalnych bibliotek i wspaniale jest mieć więcej wglądu w podstawowe mechanizmy. –

+0

Dzięki, @ThijsvanDien! To była interesująca dyskusja! – marr75

2
mysum = 0 
for a, b in izip(u, v) : 
    mysum += (a-b)**2 

o 35% szybciej niż # 3

PS: czy próbowałeś Cython (nie CPython) lub Shedskin?

+0

Dla jakich danych wejściowych? Kiedy testuję z CPython 2.7.2 z 'u = [random.random() dla _ w zakresie (10000)]' i tym samym dla 'v',' timeit (f, number = 10000) 'dla twojej wersji to 29,80 w porównaniu 29,84, ledwie różnica. (I nr 2 to 24,28). – abarnert

+0

Użyłem krotki 5 elementów. W końcu są wektorami. Nie można znaleźć 10000 wymiarów nawet w teorii strun :-P –

+0

@Thijs van Dien: Przetestowałem to w Pythonie 2.7.3, używając cProfile.run() zamiast timeit.timeit(). –

Powiązane problemy