Możesz używać <<
do mnożenia i >>
dzielenia liczb w pythonie, kiedy je mierzę, stwierdzam, że użycie binarnego przesunięcia jest 10 razy szybsze niż dzielenie lub mnożenie w zwykły sposób.
Dlaczego używa <<
i >>
jest dużo szybszy niż *
i /
?
Jakie procesy stoją za sceną *
i są /
tak powolne?
operators
bitwise-operators
Crizly
źródło
źródło
Odpowiedzi:
Przyjrzyjmy się dwóm małym programom C, które zmieniają się nieco i dzielą.
Następnie są one kompilowane,
gcc -S
aby zobaczyć, jaki będzie rzeczywisty zestaw.W wersji z przesunięciem bitowym od wezwania
atoi
do powrotu:Podczas podziału wersji:
Wystarczy spojrzeć na to i istnieje kilka innych instrukcji w wersji divide w porównaniu do przesunięcia bitów.
Kluczem jest to, co oni robią?
W wersji z przesunięciem bitów kluczową instrukcją jest
shll $2, %eax
przesunięcie logiczne w lewo - istnieje podział, a wszystko inne przesuwa wartości.W wersji dzielącej możesz zobaczyć
idivl %r8d
- ale tuż nad tym jestcltd
(zamień długi na podwójny) i pewną dodatkową logikę wokół wycieku i przeładowania. Ta dodatkowa praca, wiedząc, że mamy do czynienia z matematyką, a nie z bitami, jest często konieczna, aby uniknąć różnych błędów, które mogą wystąpić, wykonując tylko matematykę bitową.Zróbmy szybkie pomnożenie:
Zamiast przejść przez to wszystko, jest jedna linia inna:
Tutaj kompilator był w stanie stwierdzić, że matematyki można dokonać za pomocą przesunięcia, jednak zamiast przesunięcia logicznego dokonuje przesunięcia arytmetycznego. Różnica między nimi byłaby oczywista, gdybyśmy je uruchomili -
sarl
zachowuje znak. Tak więc,-2 * 4 = -8
podczas gdyshll
nie.Spójrzmy na to w szybkim skrypcie perla:
Wynik:
Um ...
-4 << 2
to18446744073709551600
nie jest dokładnie to, czego się prawdopodobnie spodziewasz w przypadku mnożenia i dzielenia. Ma rację, ale nie jest mnożeniem liczb całkowitych.I dlatego uważaj na przedwczesną optymalizację. Pozwól, aby kompilator zoptymalizował się dla Ciebie - wie, co naprawdę próbujesz zrobić i prawdopodobnie wykona to lepiej, z mniejszą liczbą błędów.
źródło
<< 2
ze* 4
i>> 2
ze/ 4
zachować kierunki SHIFT taka sama w obrębie każdego przykładu.Istniejące odpowiedzi tak naprawdę nie dotyczyły sprzętowej strony rzeczy, więc tutaj jest trochę pod tym kątem. Powszechnie wiadomo, że mnożenie i dzielenie są znacznie wolniejsze niż przesuwanie, ale dzisiejsza historia jest bardziej dopracowana.
Na przykład z pewnością prawdą jest, że mnożenie jest bardziej złożoną operacją do wdrożenia sprzętowego, ale nie zawsze kończy się wolniej . Jak się okazuje,
add
jest również znacznie bardziej skomplikowany do wdrożenia niżxor
(lub ogólnie każda operacja bitowa), aleadd
(isub
) zwykle otrzymuje wystarczającą liczbę tranzystorów poświęconych ich działaniu, które w końcu są tak samo szybkie jak operatory bitowe. Nie można więc patrzeć na złożoność implementacji sprzętu jako wskazówkę dotyczącą szybkości.Przyjrzyjmy się zatem szczegółowo przesunięciom w porównaniu z „pełnymi” operatorami, takimi jak mnożenie i przesuwanie.
Przeniesienie
Na prawie całym sprzęcie przesuwanie o stałą wartość (tj. O wartość, którą kompilator może określić w czasie kompilacji) jest szybkie . W szczególności zwykle dzieje się to z opóźnieniem jednego cyklu i przy przepustowości 1 na cykl lub lepszej. Na niektórych urządzeniach (np. Niektóre układy Intel i ARM) pewne przesunięcia o stałą mogą być nawet „wolne”, ponieważ można je wbudować w inną instrukcję (
lea
w Intelu specjalne zdolności zmiany pierwszego źródła w ARM).Przesunięcie o zmienną wartość jest bardziej szarym obszarem. Na starszych urządzeniach było to czasami bardzo wolne, a prędkość zmieniała się z pokolenia na pokolenie. Na przykład w początkowej wersji P4 Intela przesunięcie o zmienną kwotę było notorycznie wolne - wymagało czasu proporcjonalnego do wielkości przesunięcia! Na tej platformie stosowanie mnożenia w celu zastąpienia zmian może być opłacalne (tzn. Świat wywrócił się do góry nogami). W przypadku wcześniejszych układów Intela, a także kolejnych generacji, zmiana o zmienną liczbę nie była tak bolesna.
W obecnych układach Intela przesuwanie o zmienną kwotę nie jest szczególnie szybkie, ale też nie jest straszne. Architektura x86 jest hamowana, jeśli chodzi o zmienne przesunięcia, ponieważ zdefiniowali operację w nietypowy sposób: przesunięcia o wartości 0 nie modyfikują flag warunków, ale wszystkie inne przesunięcia tak. Utrudnia to efektywną zmianę nazwy rejestru flag, ponieważ nie można ustalić, dopóki przesunięcie nie wykona, czy kolejne instrukcje powinny odczytać kody warunków zapisane przez przesunięcie lub niektóre wcześniejsze instrukcje. Co więcej, przesunięcia zapisują tylko do części rejestru flag, co może powodować częściowe zatrzymanie flag.
Wynik jest taki, że w ostatnich architekturach Intela przesunięcie o zmienną kwotę zajmuje trzy „mikrooperacje”, podczas gdy większość innych prostych operacji (dodawanie, bitowe operacje, a nawet mnożenie) zajmuje tylko 1. Takie przesunięcia mogą być wykonywane co najwyżej raz na 2 cykle .
Mnożenie
Trend w nowoczesnym sprzęcie do komputerów stacjonarnych i laptopów sprawia, że mnożenie jest szybkim działaniem. W przypadku najnowszych układów Intel i AMD w rzeczywistości można wydać jedno zwielokrotnienie w każdym cyklu (nazywamy to wzajemnością ). Opóźnienia jednak z mnożeniem 3 cykle. Oznacza to, że otrzymujesz wynik dowolnego mnożenia 3 cykle po jego uruchomieniu, ale możesz rozpocząć nowe mnożenie w każdym cyklu. Która wartość (1 cykl lub 3 cykle) jest ważniejsza, zależy od struktury twojego algorytmu. Jeśli mnożenie jest częścią krytycznego łańcucha zależności, opóźnienie jest ważne. Jeśli nie, wzajemna przepustowość lub inne czynniki mogą być ważniejsze.
Kluczową kwestią jest to, że w nowoczesnych układach laptopów (lub lepszych) mnożenie jest szybką operacją i prawdopodobnie będzie szybsze niż sekwencja instrukcji 3 lub 4, którą wydałby kompilator, aby „uzyskać zaokrąglenie” dla przesunięć o zmniejszonej sile. W przypadku zmiennych przesunięć, w przypadku Intela, zwielokrotnianie byłoby ogólnie preferowane ze względu na wyżej wymienione problemy.
Na mniejszych platformach kompaktowych mnożenie może być jeszcze wolniejsze, ponieważ zbudowanie pełnego i szybkiego 32-bitowego, a szczególnie 64-bitowego mnożnika wymaga dużo tranzystorów i mocy. Jeśli ktoś mógłby podać szczegółowe informacje na temat wydajności zwielokrotniania na ostatnich mobilnych chipach, byłoby to bardzo mile widziane.
Podzielić
Podział jest zarówno bardziej złożoną operacją, pod względem sprzętowym, jak i zwielokrotnieniem, a także jest znacznie mniej powszechny w rzeczywistym kodzie - co oznacza, że prawdopodobnie przydzielono mu mniej zasobów. Trend nowoczesnych żetonów wciąż prowadzi do szybszych dzielników, ale nawet nowoczesne żetony z najwyższej półki potrzebują 10-40 cykli, aby dokonać podziału i są tylko częściowo przetwarzane. Ogólnie podziały 64-bitowe są nawet wolniejsze niż podziały 32-bitowe. W przeciwieństwie do większości innych operacji, podział może zająć zmienną liczbę cykli w zależności od argumentów.
Unikaj podziałów i zastępuj je zmianami (lub pozwól kompilatorowi to zrobić, ale może być konieczne sprawdzenie zestawu), jeśli możesz!
źródło
BINARY_LSHIFT i BINARY_RSHIFT są prostszymi procesami algorytmicznymi niż BINARY_MULTIPLY i BINARY_FLOOR_DIVIDE i mogą wymagać mniejszej liczby cykli zegara. To znaczy, jeśli masz dowolną liczbę binarną i potrzebujesz przesunięcia bitów o N, wszystko, co musisz zrobić, to przesunąć cyfry na tyle spacji i zastąpić je zerami. Mnożenie binarne jest na ogół bardziej skomplikowane , chociaż techniki takie jak mnożnik Dadda sprawiają, że jest dość szybki.
To prawda, że kompilator optymalizujący może rozpoznać przypadki, gdy pomnożymy / podzielimy przez potęgę dwóch i zastąpimy je odpowiednim przesunięciem w lewo / w prawo. Patrząc na zdemontowany kod bajtu, python najwyraźniej nie robi tego:
Jednak w moim procesorze uważam, że mnożenie i przesunięcie w lewo / w prawo mają podobne czasy, a podział podłogi (przez potęgę dwóch) jest o około 25% wolniejszy:
źródło