Ich frage nicht, wie das mathematische Konzept des Moduls funktioniert oder wie 20 % 3 == 2 Ich verstehe das gut. Ich bin neugieriger, wie der Compiler / Interpreter dies feststellt. Ich kann mir einen sehr naiven Weg vorstellen, dies so zu tun:

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

(dies erfordert natürlich einige weitere Fälle, da a kleiner als b sein kann, aber Sie haben die Idee.) Dies hätte jedoch einige schwerwiegende Leistungsprobleme zur Folge. Ich habe C ++ zusammengestellt, um die Effizienz beider Module zu testen Operator und meine naive Funktion.

#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"; } 

und die Ausgabe lautet:

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. 

Die Der Moduloperator variierte zwischen 4.000 und 70.000 Nanosekunden. Meine Funktion gewann immer bei den kleinen Zahlen, dauerte aber bei den großen Zahlen ungefähr 2 Sekunden. Da der Moduloperator so viel konsistenter ist, gehe ich davon aus, dass er die Zahl auf bitweiser Ebene vergleicht.

Wie funktioniert der Moduloperator? Ich kann mir einige Fälle vorstellen, die sehr einfach zu bestimmen wären. Mit Mod 2 können Sie beispielsweise nur die sehr genau betrachten letztes Byte. Oder mit Mod 4 kannst du dir das vorletzte ansehen b yte. Aber nicht alle Zahlen haben ein so einfaches Muster.

Kommentare

  • Auf PCs erfolgt dies im Allgemeinen in Hardware (x86 DIV und IDIV Anweisungen, die eine Ganzzahldivision ausführen, speichern auch den Rest in einem Register. Fragen Sie, wie die Hardware funktioniert?
  • Verwandte Lektüre: Assemblersprache – Wie wird Modulo ausgeführt?
  • Die lange Division funktioniert bei Binärzahlen genauso wie bei Dezimalzahlen. Probieren Sie es aus und überlegen Sie, wie Sie es codieren könnten (Hinweis: ‚ wird viele Schichten verwenden).
  • @DJMcMayhem: Ja. Manchmal muss der Compiler einige Anpassungen an den Ergebnissen vornehmen, um der Sprachspezifikation für signierte Arithmetik zu entsprechen. Im Allgemeinen ist die Geschwindigkeit der Hardwareteilung so gut, dass es besser ist, die Hardware dies anstelle der Software-Emulation tun zu lassen, wenn Sie die Eingabewerte zur Kompilierungszeit nicht kennen. Wenn der Divisor jedoch zur Kompilierungszeit bekannt ist, gibt es Tricks, die schneller ausgeführt werden können. Siehe: this und this .
  • Beachten Sie, dass Sie so sind Timing-Dinge, die Sie wirklich nur messen, wie lange es dauert, cout << die meisten Operationen durchzuführen. Ich bezweifle, dass Sie tatsächlich die Zeit messen können, die der Prozessor benötigt, um 20 % 3 auszuführen, selbst wenn der Compiler ‚ dies sowieso nicht zur Kompilierungszeit getan hat .

Antwort

Fast alle CPUs haben einen einzigen Befehl, der den Modul eines Werts zurückgibt. Betrachten Sie beispielsweise dieses Programm:

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

Wenn ich dies auf meinem Intel OS X-Computer mit g ++ -S kompiliere, ist das Ergebnis ein Boilerplate und dies:

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 

Der tatsächliche Modul erfolgt mit dieser Anweisung: idivl %ecx. Wenn dieser Befehl ausgeführt wird, wird der Quotient in %eax und der Rest in %edx platziert.

Da dies wirklich bedeutet, dass die Operation % nur einige Taktzyklen benötigt, von denen der Großteil die Daten tatsächlich in das richtige Register verschiebt. Beachten Sie auch, dass bei mindestens Intel derselbe Vorgang sowohl den Quotienten als auch den Rest findet. In Bezug auf Ihren Kommentar / und % nimm genau die gleiche Zeit. Es ist die gleiche Operation. Das einzige, was sich ändert, ist das Register, aus dem der Compiler die Ergebnisse erhält.

Bei jeder CPU, die in den letzten Jahrzehnten hergestellt wurde, können Sie davon ausgehen, dass jede grundlegende mathematische Operation (einschließlich Dinge, die wie Bibliotheksfunktionen aussehen, wie sqrt oder cos) ist eigentlich eine einzelne Maschinenanweisung und dauert im Allgemeinen höchstens einige Taktzyklen.

[UPDATE]

Wie die Leute in den Kommentaren bemerkt haben, sehen Sie, dass sich etwas einem korrekten Timing nähert Die Ausgabe muss wie folgt aus dem zeitgesteuerten Abschnitt entfernt werden:

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

Aber selbst das ist wahrscheinlich nicht genau, da die tatsächliche Granularität der Zeitabläufe möglicherweise über dem liegt, was Sie sind versuchen zu zeit. Stattdessen möchten Sie möglicherweise Folgendes tun:

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; 

Teilen Sie dann Ihr Timing durch 1.000.000. (Dies ist etwas hoch, da es die Zeit enthält, die für eine Zuweisung, einen Vergleich und ein Inkrement benötigt wird.) Auf meinem Computer ergibt dies eine Zeit von 5 Nanosekunden.

Kommentare

  • Wirklich? Mir wurde gesagt, dass sqrt eine sehr langsame Anweisung ist.
  • Siehe stackoverflow.com / question / 7724061 / …
  • @StevenBurnap Diese Antwort ist aufgrund der Architektur im Jahr 2011 etwas veraltet. Laut Tabelle von Agner Fog ‚, die mindestens einmal jährlich aktualisiert wird , Haswell SSE2-Anweisungen SQRTSS / SQRTPS (32-Bit-Float, mit 23 Signifikanzbits) hat eine Latenz von 11 Zyklen, SQRTSD / SQRTPD (64-Bit-Float) hat eine Latenz von 16 Zyklen. Diese Anweisungen sind die genauen, d. H. Nicht die schnellen Approximationsvarianten. In älteren Architekturen ist es in der Tat viel langsamer. Denken Sie auch daran, dass beim Aufrufen von std::sqrt() in den meisten C ++ – Compilern ein Bibliotheksaufruf ausgelöst wird.
  • Compiler können in besonderen Fällen zusätzlich eine Optimierung zur Festigkeitsreduzierung anwenden. Zum Beispiel könnte x% 2 für eine Bitmaske optimiert werden, wenn das ‚ auf einer bestimmten Architektur schneller ist.
  • @rwong Ich hätte gedacht, dass std:sqrt wird normalerweise inline (und verwendet Inline-Assembly). Aber ‚ habe ich nie überprüft.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.