Nie pytam o to, jak działa matematyczne pojęcie modułu ani jak 20 % 3 == 2 Dobrze to rozumiem. Jestem bardziej ciekawy, w jaki sposób kompilator / interpreter to określa. Przychodzi mi na myśl bardzo sposób na zrobienie tego, na przykład:

int modulus(int a, int b) { while (a > b) { a -= b; } return a; } 

(oczywiście wymaga to kilku dodatkowych przypadków, ponieważ a może być mniejsze niż b, ale masz pomysł.) Ale spowodowałoby to poważne problemy z wydajnością. Dodałem razem trochę C ++, aby przetestować wydajność obu modułów operator i moja naiwna funkcja.

#include <iostream> #include <chrono> #include <limits> #include "chrono_io" using namespace std; typedef chrono::high_resolution_clock Clock; int mod(int a, int b) { while (a > b) { a -= b; } return a; } int main() { auto start = Clock::now(); cout << 20 % 3 << endl; auto end = Clock::now(); cout << "Modulus on a small number took " << end - start << " seconds to calculate.\n"; start = Clock::now(); cout << numeric_limits<int>::max() % 3 << endl; end = Clock::now(); cout << "Modulus on a large number took " << end - start << " seconds to calculate.\n"; start = Clock::now(); cout << mod(20, 3) << endl; end = Clock::now(); cout << "My modulus function on a small number took " << end - start << " seconds to calculate.\n"; start = Clock::now(); cout << mod(numeric_limits<int>::max(), 3) << endl; end = Clock::now(); cout << "My modulus function on a large number took " << end - start << " seconds to calculate.\n"; } 

a wynik to:

2 Modulus on a small number took 43629 nanoseconds seconds to calculate. 1 Modulus on a large number took 5644 nanoseconds seconds to calculate. 2 My modulus function on a small number took 3682 nanoseconds seconds to calculate. 1 My modulus function on a large number took 2153824554 nanoseconds seconds to calculate. 

operator modułu wahał się od 4 000 do 70 000 nanosekund. Moja funkcja zawsze wygrywała na małych liczbach, ale zajęła około 2 sekundy na dużej liczbie. Ponieważ operator modułu jest o wiele bardziej spójny, zakładam, że porównuje liczbę na poziomie bitowym.

Jak więc działa operator modułu? Mogę wymyślić kilka przypadków, które byłyby bardzo łatwe do określenia. Na przykład w przypadku mod 2 można po prostu spojrzeć na bardzo ostatni bajt.Lub za pomocą mod 4 możesz spojrzeć na przedostatnią b yte. Ale nie wszystkie liczby mają taki prosty wzór.

Komentarze

  • Na komputerach PC odbywa się to zazwyczaj sprzętowo (x86 DIV i IDIV, które wykonują dzielenie liczb całkowitych, również przechowują resztę w rejestrze). Zastanawiasz się, jak działa sprzęt ?
  • Powiązane artykuły: Język asemblera – jak to zrobić Modulo?
  • Dzielenie długie działa tak samo z liczbami binarnymi, jak z dziesiętnymi. Spróbuj, a potem pomyśl, jak możesz to zakodować (wskazówka: ' będziesz używać wielu zmian).
  • @DJMcMayhem: Tak. Czasami kompilator musi dokonać pewnych korekt wyników, aby zachować zgodność ze specyfikacją języka dla arytmetyki ze znakiem. Ogólnie rzecz biorąc, szybkość podziału sprzętu jest na tyle dobra, że jeśli nie znasz wartości wejściowych w czasie kompilacji, lepiej pozwolić, aby robił to sprzęt, zamiast emulacji programowej. Jeśli jednak dzielnik jest znany w czasie kompilacji, istnieją sztuczki, które mogą działać szybciej. Zobacz: to i to .
  • Zwróć uwagę, że sposób mierzysz czas, po prostu mierzysz czas potrzebny do cout << większości operacji. Wątpię, czy rzeczywiście mógłbyś zmierzyć czas potrzebny procesorowi na wykonanie 20 % 3, nawet jeśli kompilator nie ' i tak nie zrobił tego w czasie kompilacji .

Odpowiedź

Prawie wszystkie procesory mają jedną instrukcję, która zwraca moduł wartości. Weźmy na przykład pod uwagę ten program:

int main() { int i = 10; return i % 3; } 

Jeśli skompiluję go na moim komputerze z systemem Intel OS X przy użyciu g ++ -S, w rezultacie otrzymam szablon i to:

movl $3, %eax movl $0, -4(%rbp) movl $10, -8(%rbp) movl -8(%rbp), %ecx movl %eax, -12(%rbp) ## 4-byte Spill movl %ecx, %eax cltd movl -12(%rbp), %ecx ## 4-byte Reload idivl %ecx movl %edx, %eax 

Rzeczywisty moduł uzyskuje się zgodnie z tą instrukcją: idivl %ecx. Gdy ta instrukcja zostanie wykonana, iloraz zostanie umieszczony w %eax, a reszta w %edx.

Ponieważ tak naprawdę oznacza to, że operacja % zajmie tylko kilka cykli zegara, z których większość przenosi dane do właściwego rejestru. Zauważ też, że przynajmniej w przypadku Intela ta sama operacja pozwala znaleźć zarówno iloraz, jak i resztę, więc w odniesieniu do Twojego komentarza / i % biorę dokładnie ten sam czas. To ta sama operacja. Zmienia się tylko to, z jakiego rejestru kompilator pobiera wyniki.

W przypadku dowolnego procesora wykonanego w ciągu ostatnich kilku dekad można założyć, że każda podstawowa operacja matematyczna (w tym rzeczy wyglądające jak funkcje biblioteczne, takie jak sqrt lub cos) jest w rzeczywistości pojedynczą instrukcją maszynową i zwykle zajmuje najwyżej kilka cykli zegara.

[AKTUALIZACJA]

Jak ludzie zauważyli w komentarzach, aby zobaczyć, że coś zbliża się w odpowiednim momencie, musisz usunąć dane wyjściowe z sekcji czasowej w ten sposób:

int i; auto start = Clock::now(); i = 20 % 3; auto end = Clock::now(); cout << i << endl; 

Ale nawet to prawdopodobnie nie jest dokładne, ponieważ rzeczywista szczegółowość czasów może przekraczać to, co jesteś próbując czasu. Zamiast tego możesz to zrobić:

int i=0; int x; auto start = Clock::now(); for(i=0;i<1000000;i++) x = i % 3; auto end = Clock::now(); cout << i << endl; 

Następnie podziel swój czas przez 1 000 000. (Będzie to trochę za dużo, ponieważ obejmuje czas potrzebny na zadanie, porównanie i przyrost). Na moim komputerze daje to czas 5 nanosekund.

Komentarze

  • Naprawdę? Powiedziano mi, że sqrt to bardzo powolna instrukcja.
  • Zobacz stackoverflow.com / questions / 7724061 / …
  • @StevenBurnap ta odpowiedź jest nieco nieaktualna, oparta na architekturze z 2011 roku. Według Agner Fog ' tabela, która jest aktualizowana co najmniej raz w roku , instrukcje Haswell SSE2 SQRTSS / SQRTPS (32-bitowa liczba zmiennoprzecinkowa, z 23 bitami istotności) ma opóźnienie 11 cykli, SQRTSD / SQRTPD (64-bitowa liczba zmiennoprzecinkowa) ma opóźnienie 16 cykli. Instrukcje te są dokładne, tj. Nie należą do wariantów szybkiego przybliżenia. Rzeczywiście, jest dużo wolniej w starszych architekturach. Pamiętaj też, że wywołanie std::sqrt() powoduje wywołanie biblioteki w większości kompilatorów C ++.
  • Kompilatory mogą dodatkowo zastosować optymalizację redukcji siły w szczególnych przypadkach. Na przykład x% 2 może zostać zoptymalizowany do maski bitowej, jeśli ' jest szybsze na określonej architekturze.
  • @rwong Pomyślałbym, że std:sqrt byłby zwykle wbudowany (i używa zestawu wbudowanego). Ale ja ' nigdy nie sprawdzałem.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *