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
.
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' –
Skomentował, że jest szybszy jako lista niż generator. Wygląda na to, że wie, co robi. – FogleBird
@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). –