Czy w rzeczywistości szybsze jest użycie powiedz (i << 3) + (i << 1) do pomnożenia przez 10 niż bezpośrednie użycie i * 10?
Może, ale nie musi być na twoim komputerze - jeśli cię to obchodzi, zmierz swoje rzeczywiste użycie.
Studium przypadku - od 486 do rdzenia i7
Benchmarking jest bardzo trudny do przeprowadzenia w sensowny sposób, ale możemy spojrzeć na kilka faktów. Z http://www.penguin.cz/~literakl/intel/s.html#SAL i http://www.penguin.cz/~literakl/intel/i.html#IMUL otrzymujemy pojęcie o cyklach zegara x86 potrzebne do przesunięcia arytmetycznego i mnożenia. Powiedzmy, że trzymamy się „486” (najnowszego wymienionego), 32-bitowych rejestrów i natychmiastowych, IMUL zajmuje 13-42 cykli i IDIV 44. Każda SAL zajmuje 2 i dodaje 1, więc nawet przy kilku z nich zmienia się powierzchownie jak zwycięzca.
Obecnie z rdzeniem i7:
(z http://software.intel.com/en-us/forums/showthread.php?t=61481 )
Opóźnienie wynosi 1 cykl dla dodawania liczb całkowitych i 3 cykle dla mnożenia liczb całkowitych . Opóźnienia i przelotność można znaleźć w załączniku C „Podręcznika optymalizacji architektury Intel® 64 i IA-32”, który znajduje się na stronie http://www.intel.com/products/processor/manuals/ .
(z jakiegoś napadu Intela)
Korzystając z SSE, Core i7 może wydawać jednoczesne instrukcje dodawania i mnożenia, co daje szczytową szybkość 8 operacji zmiennoprzecinkowych (FLOP) na cykl zegara
To daje wyobrażenie o tym, jak daleko zaszło. Ciekawostki związane z optymalizacją - takie jak przesunięcie bitów w porównaniu do *
- które zostały potraktowane poważnie nawet w latach 90., są teraz przestarzałe. Przesunięcie bitów jest wciąż szybsze, ale w przypadku braku mocy dwóch mul / dz, zanim wykonasz wszystkie swoje zmiany i dodasz wyniki, będzie znowu wolniejszy. Następnie więcej instrukcji oznacza więcej błędów pamięci podręcznej, więcej potencjalnych problemów w przetwarzaniu potokowym, większe wykorzystanie rejestrów tymczasowych może oznaczać więcej zapisywania i przywracania zawartości rejestru ze stosu ... szybko staje się zbyt skomplikowane, aby ostatecznie obliczyć wszystkie wpływy, ale są one głównie negatywne.
funkcjonalność w kodzie źródłowym a implementacja
Mówiąc bardziej ogólnie, twoje pytanie jest oznaczone jako C i C ++. Jako języki trzeciej generacji są one specjalnie zaprojektowane, aby ukryć szczegóły podstawowego zestawu instrukcji procesora. Aby spełnić ich Standardy językowe, muszą obsługiwać operacje mnożenia i przenoszenia (i wiele innych), nawet jeśli nie obsługuje tego sprzęt . W takich przypadkach muszą zsyntetyzować wymagany wynik przy użyciu wielu innych instrukcji. Podobnie muszą zapewniać obsługę oprogramowania dla operacji zmiennoprzecinkowych, jeśli procesor go nie ma i nie ma FPU. Nowoczesne procesory wszystkie obsługują *
i<<
, więc może się to wydawać absurdalnie teoretyczne i historyczne, ale istotne jest to, że swoboda wyboru implementacji przebiega w obie strony: nawet jeśli procesor posiada instrukcję, która implementuje operację wymaganą w kodzie źródłowym w ogólnym przypadku, kompilator może wybierz coś innego, co woli, ponieważ jest to lepsze w konkretnym przypadku, z którym ma do czynienia kompilator.
Przykłady (z hipotetycznym językiem asemblera)
source literal approach optimised approach
#define N 0
int x; .word x xor registerA, registerA
x *= N; move x -> registerA
move x -> registerB
A = B * immediate(0)
store registerA -> x
...............do something more with x...............
Instrukcje takie jak wyłączne lub ( xor
) nie mają związku z kodem źródłowym, ale xor-cokolwiek ze sobą usuwa wszystkie bity, więc można go użyć do ustawienia czegoś na 0. Kod źródłowy sugerujący, że adresy pamięci nie wymagają użycia.
Tego rodzaju włamania były używane tak długo, jak długo istniały komputery. Na początku 3GLs, aby zabezpieczyć programistę, wyjście kompilatora musiało zaspokoić istniejącego hardcorowego, optymalizującego ręcznie dewelopera w asemblerze. społeczność, że wygenerowany kod nie był wolniejszy, bardziej szczegółowy lub w inny sposób gorszy. Kompilatory szybko przyjęły wiele świetnych optymalizacji - stały się lepiej scentralizowanym magazynem, niż mógłby to być każdy programista w języku asemblera, choć zawsze istnieje szansa, że przegapią konkretną optymalizację, która jest kluczowa w konkretnym przypadku - ludzie mogą czasem wykręć to i szukaj czegoś lepszego, podczas gdy kompilatory robią tak, jak im powiedzono, dopóki ktoś nie wróci do nich tym doświadczeniem.
Tak więc, nawet jeśli przesuwanie i dodawanie jest jeszcze szybsze na określonym sprzęcie, pisarz kompilatora prawdopodobnie zadziałał dokładnie wtedy, gdy jest bezpieczny i korzystny.
Konserwowalność
Jeśli twój sprzęt ulegnie zmianie, możesz go ponownie skompilować, a on spojrzy na docelowy procesor i dokona innego najlepszego wyboru, podczas gdy raczej nie będziesz chciał ponownie przeglądać swoich „optymalizacji” lub listy, które środowiska kompilacji powinny używać mnożenia, a które powinny się zmieniać. Pomyśl o wszystkich „optymalizacjach” o przesunięciu nieco dwóch bitów, napisanych ponad 10 lat temu, które spowalniają kod, w którym się znajdują, ponieważ działa na nowoczesnych procesorach ...!
Na szczęście dobre kompilatory, takie jak GCC, mogą zazwyczaj zastąpić serię przesunięć bitowych i arytmetyki bezpośrednim zwielokrotnieniem, gdy włączona jest jakakolwiek optymalizacja (tj. ...main(...) { return (argc << 4) + (argc << 2) + argc; }
-> imull $21, 8(%ebp), %eax
), więc rekompilacja może pomóc nawet bez poprawiania kodu, ale nie jest to gwarantowane.
Dziwny kod bitshiftingu, który implementuje mnożenie lub dzielenie, jest o wiele mniej wyrazisty w stosunku do tego, co próbujesz osiągnąć koncepcyjnie, więc inni programiści będą tym zdezorientowani, a zdezorientowany programista bardziej prawdopodobne jest wprowadzenie błędów lub usunięcie czegoś niezbędnego w celu przywrócenia zdrowego rozsądku. Jeśli robisz rzeczy nieoczywiste tylko wtedy, gdy są naprawdę namacalnie korzystne, a następnie dobrze je dokumentujesz (ale i tak nie dokumentujesz innych rzeczy, które są intuicyjne), wszyscy będą szczęśliwsi.
Rozwiązania ogólne a rozwiązania częściowe
Jeśli masz trochę dodatkowej wiedzy, na przykład, że int
naprawdę będziesz przechowywać tylko wartości x
, y
a z
następnie możesz być w stanie wypracować instrukcje, które działają dla tych wartości i uzyskać wynik szybciej niż wtedy, gdy kompilator nie ma ten wgląd i wymaga implementacji, która działa dla wszystkich int
wartości. Rozważ na przykład swoje pytanie:
Mnożenie i dzielenie można osiągnąć za pomocą operatorów bitów ...
Ilustrujesz mnożenie, ale co powiesz na podział?
int x;
x >> 1; // divide by 2?
Zgodnie ze standardem C ++ 5.8:
-3- Wartość E1 >> E2 to E1 z przesuniętymi w prawo pozycjami bitów E2. Jeśli E1 ma typ bez znaku lub jeśli E1 ma typ ze znakiem i nieujemną wartość, wartość wyniku jest integralną częścią ilorazu E1 podzielonego przez liczbę 2 podniesioną do potęgi E2. Jeśli E1 ma typ ze znakiem i wartość ujemną, wynikowa wartość jest zdefiniowana w implementacji.
Zatem przesunięcie bitów ma wynik zdefiniowany w implementacji, gdy x
jest ujemny: może nie działać w ten sam sposób na różnych komputerach. Ale /
działa o wiele bardziej przewidywalnie. (Może to również nie być całkowicie spójne, ponieważ różne maszyny mogą mieć różne reprezentacje liczb ujemnych, a zatem różne zakresy, nawet jeśli ta sama liczba bitów tworzy tę reprezentację.)
Możesz powiedzieć: „Nie obchodzi mnie to… int
przechowywanie wieku pracownika, nigdy nie może być negatywne”. Jeśli masz taki szczególny wgląd, to tak - Twoja >>
bezpieczna optymalizacja może zostać pominięta przez kompilator, o ile nie zrobisz tego wprost w kodzie. Jest to jednak ryzykowne i rzadko przydatne, ponieważ przez większość czasu nie będziesz mieć takiego wglądu, a inni programiści pracujący nad tym samym kodem nie będą wiedzieć, że postawiłeś dom na pewne niezwykłe oczekiwania dotyczące danych, które „ Zajmę się ... to, co wydaje się całkowicie bezpieczną zmianą, może się nie udać z powodu twojej „optymalizacji”.
Czy jest jakiś rodzaj danych wejściowych, których nie można pomnożyć ani podzielić w ten sposób?
Tak ... jak wspomniano powyżej, liczby ujemne mają zachowanie zdefiniowane w implementacji, gdy są „podzielone” przez przesunięcie bitów.