Ist es tatsächlich schneller, say (i << 3) + (i << 1) zu verwenden, um mit 10 zu multiplizieren, als i * 10 direkt zu verwenden?
Möglicherweise befindet es sich auf Ihrem Computer oder nicht. Wenn Sie sich darum kümmern, messen Sie Ihren tatsächlichen Verbrauch.
Eine Fallstudie - von 486 bis Core i7
Benchmarking ist sehr schwierig sinnvoll durchzuführen, aber wir können uns einige Fakten ansehen. Unter http://www.penguin.cz/~literakl/intel/s.html#SAL und http://www.penguin.cz/~literakl/intel/i.html#IMUL erhalten wir eine Vorstellung von x86-Taktzyklen benötigt für arithmetische Verschiebung und Multiplikation. Nehmen wir an, wir halten uns an "486" (das neueste aufgelistete), 32-Bit-Register und sofort, IMUL benötigt 13-42 Zyklen und IDIV 44. Jeder SAL benötigt 2 und addiert 1, so dass selbst wenn einige von ihnen zusammen oberflächlich aussehen, dies aussieht wie ein Gewinner.
In diesen Tagen mit dem Kern i7:
(von http://software.intel.com/en-us/forums/showthread.php?t=61481 )
Die Latenz beträgt 1 Zyklus für eine Ganzzahladdition und 3 Zyklen für eine Ganzzahlmultiplikation . Die Latenzen und den Durchsatz finden Sie in Anhang C des "Referenzhandbuchs zur Optimierung von Intel® 64- und IA-32-Architekturen" unter http://www.intel.com/products/processor/manuals/ .
(von einem Intel Klappentext)
Mit SSE kann der Core i7 simultane Additions- und Multiplikationsbefehle ausgeben, was zu einer Spitzenrate von 8 Gleitkommaoperationen (FLOP) pro Taktzyklus führt
Das gibt Ihnen eine Vorstellung davon, wie weit die Dinge gekommen sind. Die Optimierungs-Trivia - wie Bit Shifting versus *
-, die bis in die 90er Jahre ernst genommen wurden, sind jetzt einfach veraltet. Die Bitverschiebung ist immer noch schneller, aber für Nicht-Zweierpotenzen (Mul / Div) ist es wieder langsamer, wenn Sie alle Verschiebungen durchführen und die Ergebnisse hinzufügen. Dann bedeuten mehr Anweisungen mehr Cache-Fehler, mehr potenzielle Probleme beim Pipelining, mehr Verwendung temporärer Register kann mehr Speichern und Wiederherstellen von Registerinhalten aus dem Stapel bedeuten ... es wird schnell zu kompliziert, alle Auswirkungen endgültig zu quantifizieren, aber sie sind überwiegend negativ.
Funktionalität im Quellcode vs. Implementierung
Im Allgemeinen ist Ihre Frage mit C und C ++ gekennzeichnet. Als Sprachen der 3. Generation wurden sie speziell entwickelt, um die Details des zugrunde liegenden CPU-Befehlssatzes auszublenden. Um ihre Sprachstandards zu erfüllen, müssen sie Multiplikations- und Verschiebungsvorgänge (und viele andere) unterstützen, auch wenn die zugrunde liegende Hardware dies nicht tut . In solchen Fällen müssen sie das erforderliche Ergebnis unter Verwendung vieler anderer Anweisungen synthetisieren. Ebenso müssen sie Softwareunterstützung für Gleitkommaoperationen bereitstellen, wenn der CPU diese fehlt und keine FPU vorhanden ist. Moderne CPUs unterstützen *
und<<
Das mag absurd theoretisch und historisch erscheinen, aber die Bedeutung ist, dass die Freiheit, die Implementierung zu wählen, in beide Richtungen geht: Selbst wenn die CPU über eine Anweisung verfügt, die die im Quellcode im allgemeinen Fall angeforderte Operation implementiert, steht es dem Compiler frei Wählen Sie etwas anderes, das es bevorzugt, da es für den speziellen Fall, mit dem der Compiler konfrontiert ist, besser ist .
Beispiele (mit einer hypothetischen Assemblersprache)
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...............
Anweisungen wie exklusiv oder ( xor
) haben keine Beziehung zum Quellcode, aber wenn Sie irgendetwas mit sich selbst verknüpfen, werden alle Bits gelöscht, sodass etwas auf 0 gesetzt werden kann. Quellcode, der Speicheradressen impliziert, erfordert möglicherweise keine Verwendung.
Diese Art von Hacks wurde verwendet, solange es Computer gibt. In den frühen Tagen von 3GLs musste die Compiler-Ausgabe den vorhandenen Hardcore-Hand-optimierenden Assembler-Entwickler erfüllen, um die Entwickler-Aufnahme zu sichern. Community, dass der produzierte Code nicht langsamer, ausführlicher oder auf andere Weise schlechter war. Compiler haben schnell viele großartige Optimierungen vorgenommen - sie wurden zu einem besseren zentralen Speicher als jeder einzelne Assembler-Programmierer, obwohl es immer die Möglichkeit gibt, dass sie eine bestimmte Optimierung verpassen, die in einem bestimmten Fall entscheidend ist - Menschen können es manchmal Nut it out und tappen nach etwas Besserem, während Compiler einfach tun, was ihnen gesagt wurde, bis jemand diese Erfahrung in sie zurückspeist.
Selbst wenn das Verschieben und Hinzufügen auf einer bestimmten Hardware noch schneller ist, hat der Compiler-Writer wahrscheinlich genau dann geklappt, wenn es sowohl sicher als auch vorteilhaft ist.
Wartbarkeit
Wenn sich Ihre Hardware ändert, können Sie sie neu kompilieren. Sie wird sich die Ziel-CPU ansehen und eine weitere beste Wahl treffen, während Sie Ihre "Optimierungen" wahrscheinlich nie wieder besuchen oder auflisten möchten, welche Kompilierungsumgebungen Multiplikation verwenden und welche sich verschieben sollten. Denken Sie an all die bitverschobenen "Optimierungen" ohne Potenz von zwei, die vor mehr als 10 Jahren geschrieben wurden und jetzt den Code verlangsamen, in dem sie sich befinden, da er auf modernen Prozessoren ausgeführt wird ...!
Glücklicherweise können gute Compiler wie GCC in der Regel eine Reihe von Bitverschiebungen und Arithmetik durch eine direkte Multiplikation ersetzen, wenn eine Optimierung aktiviert ist (dh ...main(...) { return (argc << 4) + (argc << 2) + argc; }
-> imull $21, 8(%ebp), %eax
), sodass eine Neukompilierung auch ohne Korrektur des Codes hilfreich sein kann. Dies ist jedoch nicht garantiert.
Seltsamer Bitshifting-Code, der Multiplikation oder Division implementiert, ist weit weniger aussagekräftig für das, was Sie konzeptionell erreichen wollten. Andere Entwickler werden davon verwirrt sein, und ein verwirrter Programmierer führt eher Fehler ein oder entfernt etwas Wesentliches, um die scheinbare Vernunft wiederherzustellen. Wenn Sie nur nicht offensichtliche Dinge tun, wenn sie wirklich greifbar sind, und sie dann gut dokumentieren (aber keine anderen Dinge dokumentieren, die sowieso intuitiv sind), sind alle glücklicher.
Allgemeine Lösungen versus Teillösungen
Wenn Sie etwas mehr Wissen, wie , dass Ihr int
Wille wirklich nur Werte werden zu speichern x
, y
und z
dann können Sie in der Lage sein , einige Anweisungen zu arbeiten , dass die Arbeit für diese Werte und erhalten Sie Ihr Ergebnis schneller , als wenn der Compiler nicht haben diese Einsicht und braucht eine Implementierung, die für alle int
Werte funktioniert . Betrachten Sie zum Beispiel Ihre Frage:
Multiplikation und Division können mit Bitoperatoren erreicht werden ...
Sie veranschaulichen die Multiplikation, aber wie steht es mit der Division?
int x;
x >> 1; // divide by 2?
Nach dem C ++ Standard 5.8:
-3- Der Wert von E1 >> E2 ist E1 rechtsverschobene E2-Bitpositionen. Wenn E1 einen vorzeichenlosen Typ hat oder wenn E1 einen vorzeichenbehafteten Typ und einen nicht negativen Wert hat, ist der Wert des Ergebnisses der integrale Bestandteil des Quotienten von E1 geteilt durch die auf die Potenz E2 erhobene Größe 2. Wenn E1 einen vorzeichenbehafteten Typ und einen negativen Wert hat, ist der resultierende Wert implementierungsdefiniert.
Ihre Bitverschiebung hat also ein implementierungsdefiniertes Ergebnis, wenn sie x
negativ ist: Auf verschiedenen Computern funktioniert sie möglicherweise nicht auf die gleiche Weise. Funktioniert aber /
weitaus vorhersehbarer. (Es ist möglicherweise auch nicht perfekt konsistent, da verschiedene Maschinen unterschiedliche Darstellungen negativer Zahlen und damit unterschiedliche Bereiche haben können, selbst wenn die gleiche Anzahl von Bits die Darstellung ausmacht.)
Sie können sagen "Es ist mir egal ... das int
speichert das Alter des Mitarbeiters, es kann niemals negativ sein". Wenn Sie diese Art von besonderen Einsichten haben, dann ja - Ihre >>
sichere Optimierung wird möglicherweise vom Compiler übergeben, es sei denn, Sie tun dies ausdrücklich in Ihrem Code. Aber es ist riskant und selten nützlich, da Sie diese Art von Einsicht oft nicht haben und andere Programmierer, die an demselben Code arbeiten, nicht wissen, dass Sie das Haus auf ungewöhnliche Erwartungen an die Daten gesetzt haben, die Sie haben. Ich kümmere mich um ... was als absolut sichere Änderung an ihnen erscheint, könnte aufgrund Ihrer "Optimierung" nach hinten losgehen.
Gibt es irgendeine Art von Eingabe, die auf diese Weise nicht multipliziert oder geteilt werden kann?
Ja ... wie oben erwähnt, haben negative Zahlen ein implementierungsdefiniertes Verhalten, wenn sie durch Bitverschiebung "geteilt" werden.