Z tej strony wiemy, że:
Porównania łańcuchowe są szybsze niż korzystanie z
and
operatora. Piszx < y < z
zamiastx < y and y < z
.
Jednak otrzymałem inny wynik testowania następujących fragmentów kodu:
$ python -m timeit "x = 1.2" "y = 1.3" "z = 1.8" "x < y < z"
1000000 loops, best of 3: 0.322 usec per loop
$ python -m timeit "x = 1.2" "y = 1.3" "z = 1.8" "x < y and y < z"
1000000 loops, best of 3: 0.22 usec per loop
$ python -m timeit "x = 1.2" "y = 1.3" "z = 1.1" "x < y < z"
1000000 loops, best of 3: 0.279 usec per loop
$ python -m timeit "x = 1.2" "y = 1.3" "z = 1.1" "x < y and y < z"
1000000 loops, best of 3: 0.215 usec per loop
Wydaje się, że x < y and y < z
jest szybszy niż x < y < z
. Czemu?
Po przeszukaniu niektórych postów na tej stronie (takich jak ten ) wiem, że kluczem jest „ocenione tylko raz” x < y < z
, ale nadal jestem zdezorientowany. Aby przeprowadzić dalsze badania, zdemontowałem te dwie funkcje za pomocą dis.dis
:
import dis
def chained_compare():
x = 1.2
y = 1.3
z = 1.1
x < y < z
def and_compare():
x = 1.2
y = 1.3
z = 1.1
x < y and y < z
dis.dis(chained_compare)
dis.dis(and_compare)
A wynik to:
## chained_compare ##
4 0 LOAD_CONST 1 (1.2)
3 STORE_FAST 0 (x)
5 6 LOAD_CONST 2 (1.3)
9 STORE_FAST 1 (y)
6 12 LOAD_CONST 3 (1.1)
15 STORE_FAST 2 (z)
7 18 LOAD_FAST 0 (x)
21 LOAD_FAST 1 (y)
24 DUP_TOP
25 ROT_THREE
26 COMPARE_OP 0 (<)
29 JUMP_IF_FALSE_OR_POP 41
32 LOAD_FAST 2 (z)
35 COMPARE_OP 0 (<)
38 JUMP_FORWARD 2 (to 43)
>> 41 ROT_TWO
42 POP_TOP
>> 43 POP_TOP
44 LOAD_CONST 0 (None)
47 RETURN_VALUE
## and_compare ##
10 0 LOAD_CONST 1 (1.2)
3 STORE_FAST 0 (x)
11 6 LOAD_CONST 2 (1.3)
9 STORE_FAST 1 (y)
12 12 LOAD_CONST 3 (1.1)
15 STORE_FAST 2 (z)
13 18 LOAD_FAST 0 (x)
21 LOAD_FAST 1 (y)
24 COMPARE_OP 0 (<)
27 JUMP_IF_FALSE_OR_POP 39
30 LOAD_FAST 1 (y)
33 LOAD_FAST 2 (z)
36 COMPARE_OP 0 (<)
>> 39 POP_TOP
40 LOAD_CONST 0 (None)
Wygląda na to, że x < y and y < z
ma mniej rozproszonych poleceń niż x < y < z
. Powinienem rozważyć x < y and y < z
szybciej niż x < y < z
?
Przetestowano w języku Python 2.7.6 na procesorze Intel (R) Xeon (R) E5640 @ 2,67 GHz.
python
performance
zangw
źródło
źródło
timeit
testy, zaciekawiło mnie to.y
nie jest to tylko wyszukiwanie zmiennych, ale droższy proces, taki jak wywołanie funkcji? To10 < max(range(100)) < 15
znaczy jest szybsze niż10 < max(range(100)) and max(range(100)) < 15
ponieważmax(range(100))
jest wywoływane raz dla obu porównań.Odpowiedzi:
Różnica polega na tym, że in
x < y < z
y
jest oceniane tylko raz. Nie robi to dużej różnicy, jeśli y jest zmienną, ale robi to, gdy jest to wywołanie funkcji, której obliczenie zajmuje trochę czasu.from time import sleep def y(): sleep(.2) return 1.3 %timeit 1.2 < y() < 1.8 10 loops, best of 3: 203 ms per loop %timeit 1.2 < y() and y() < 1.8 1 loops, best of 3: 405 ms per loop
źródło
sleep()
wewnętrznej?Optymalny kod bajtowy dla obu zdefiniowanych funkcji to
0 LOAD_CONST 0 (None) 3 RETURN_VALUE
ponieważ wynik porównania nie jest używany. Uatrakcyjnijmy sytuację, zwracając wynik porównania. Niech wynik będzie niemożliwy do poznania w czasie kompilacji.
def interesting_compare(y): x = 1.1 z = 1.3 return x < y < z # or: x < y and y < z
Ponownie, obie wersje porównania są semantycznie identyczne, więc optymalny kod bajtowy jest taki sam dla obu konstrukcji. Najlepiej, jak potrafię, wyglądałoby to tak. Dodałem adnotacje do każdego wiersza z zawartością stosu przed i po każdym opkodzie, w notacji Forth (wierzchołek stosu po prawej stronie,
--
dzieli przed i po, koniec?
oznacza coś, co może tam być lub nie). Zauważ, żeRETURN_VALUE
odrzuca wszystko, co pozostało na stosie poniżej zwróconej wartości.0 LOAD_FAST 0 (y) ; -- y 3 DUP_TOP ; y -- y y 4 LOAD_CONST 0 (1.1) ; y y -- y y 1.1 7 COMPARE_OP 4 (>) ; y y 1.1 -- y pred 10 JUMP_IF_FALSE_OR_POP 19 ; y pred -- y 13 LOAD_CONST 1 (1.3) ; y -- y 1.3 16 COMPARE_OP 0 (<) ; y 1.3 -- pred >> 19 RETURN_VALUE ; y? pred --
Jeśli implementacja języka, CPython, PyPy, cokolwiek, nie generuje tego kodu bajtowego (lub własnej równoważnej sekwencji operacji) dla obu odmian, świadczy to o słabej jakości tego kompilatora kodu bajtowego . Uzyskanie z sekwencji kodu bajtowego, który wysłałeś na powyższe, jest rozwiązanym problemem (myślę, że wszystko, czego potrzebujesz w tym przypadku, to ciągłe zwijanie , eliminacja martwego kodu i lepsze modelowanie zawartości stosu; powszechna eliminacja podwyrażeń również byłaby tania i wartościowa ), i naprawdę nie ma wymówki, by nie robić tego w nowoczesnej implementacji języka.
Teraz zdarza się, że wszystkie obecne implementacje języka mają słabej jakości kompilatory kodu bajtowego. Ale powinieneś zignorować to podczas kodowania! Udawaj, że kompilator kodu bajtowego jest dobry i napisz najbardziej czytelny kod. Prawdopodobnie i tak będzie wystarczająco szybko. Jeśli tak nie jest, najpierw poszukaj ulepszeń algorytmicznych, a następnie spróbuj Cython - to zapewni znacznie większą poprawę przy tym samym wysiłku niż jakiekolwiek poprawki na poziomie wyrażenia, które możesz zastosować.
źródło
interesting_compare
kod do prostego kodu bajtowego na górze (który działałby tylko z wstawianiem). Całkowicie offtopic, ale: Inlining jest jedną z najważniejszych optymalizacji każdego kompilatora. Możesz spróbować uruchomić niektóre testy porównawcze za pomocą, powiedzmy, HotSpot na prawdziwych programach (nie na niektórych testach matematycznych, które spędzają 99% czasu w jednej gorącej pętli, która jest zoptymalizowana ręcznie [i dlatego nie ma już żadnych wywołań funkcji]), kiedy wyłączasz możliwość wstawiania czegokolwiek - zobaczysz duże regresje.interesting_compare
.y
nie zmieniają stosu, ponieważ ma wiele narzędzi do debugowania.Ponieważ różnica w wynikach wydaje się wynikać z braku optymalizacji, myślę, że w większości przypadków należy zignorować tę różnicę - może się zdarzyć, że różnica zniknie. Różnica polega na tym, że
y
powinien być oceniany tylko raz, a to rozwiązuje się, powielając go na stosie, co wymaga dodatkowej opłatyPOP_TOP
- rozwiązanie do użyciaLOAD_FAST
może być jednak możliwe.Istotna różnica polega jednak na tym, że w
x<y and y<z
drugimy
przypadku należy ocenić dwukrotnie, jeślix<y
ocena jest prawdą, ma to konsekwencje, jeśli ocenay
zajmuje dużo czasu lub ma skutki uboczne.W większości scenariuszy powinieneś używać,
x<y<z
mimo że jest nieco wolniejszy.źródło
Po pierwsze, twoje porównanie jest prawie bez znaczenia, ponieważ dwie różne konstrukcje nie zostały wprowadzone w celu zapewnienia poprawy wydajności, więc nie powinieneś na tej podstawie decydować, czy użyć jednej zamiast drugiej.
x < y < z
Konstrukt:x
,y
iz
raz i sprawdzić, czy cały warunek jest spełniony. Użycieand
zmienia semantykę, oceniający
wiele razy, co może zmienić wynik .Więc wybierz jedną zamiast drugiej w zależności od wybranej semantyki i, jeśli są równoważne, czy jedna jest bardziej czytelna od drugiej.
To powiedziawszy: bardziej zdemontowany kod nie oznacza wolniejszego kodu. Jednak wykonywanie większej liczby operacji na kodzie bajtowym oznacza, że każda operacja jest prostsza, a mimo to wymaga iteracji głównej pętli. Oznacza to, że jeśli operacje, które wykonujesz, są niezwykle szybkie (np. Wyszukiwanie lokalnych zmiennych tak jak tam robisz), wtedy obciążenie związane z wykonywaniem większej liczby operacji na kodzie bajtowym może mieć znaczenie.
Ale uwaga, że wynik ten ma nie trzymać w sytuacji bardziej ogólny jedynie do „najgorszego przypadku”, które stało się profil. Jak zauważyli inni, jeśli zmienisz
y
coś na coś, co zajmuje nawet trochę więcej czasu, zobaczysz, że wyniki się zmieniają, ponieważ notacja łańcuchowa ocenia to tylko raz.Zreasumowanie:
źródło