Ich denke, es wäre hilfreicher, wenn der Fragesteller eine differenziertere Antwort hätte, da ich in den Fragen und in einigen Antworten oder Kommentaren mehrere ungeprüfte Annahmen sehe.
Die sich ergebende relative Laufzeit von Verschiebung und Multiplikation hat nichts mit C zu tun. Wenn ich C sage, meine ich nicht die Instanz einer bestimmten Implementierung, wie der oder jener Version von GCC, sondern die Sprache. Ich möchte das nicht absurd nehmen, sondern ein extremes Beispiel zur Veranschaulichung verwenden: Sie könnten einen vollständig standardkonformen C-Compiler implementieren und die Multiplikation eine Stunde dauern lassen, während das Verschieben Millisekunden dauert - oder umgekehrt. Mir sind solche Leistungseinschränkungen in C oder C ++ nicht bekannt.
Sie interessieren sich möglicherweise nicht für diese Technik in der Argumentation. Ihre Absicht war es wahrscheinlich, nur die relative Leistung von Shifts im Vergleich zu Multiplikationen zu testen, und Sie haben C gewählt, da es im Allgemeinen als Programmiersprache auf niedriger Ebene wahrgenommen wird, sodass man erwarten kann, dass der Quellcode direkter in entsprechende Anweisungen übersetzt wird. Solche Fragen sind sehr häufig und ich denke, eine gute Antwort sollte darauf hinweisen, dass selbst in C Ihr Quellcode nicht so direkt in Anweisungen übersetzt wird, wie Sie es in einem bestimmten Fall vielleicht denken. Ich habe Ihnen nachfolgend einige mögliche Kompilierungsergebnisse gegeben.
Hier kommen Kommentare ins Spiel, die die Nützlichkeit der Substitution dieser Äquivalenz in realer Software in Frage stellen. Sie können einige in den Kommentaren zu Ihrer Frage sehen, wie zum Beispiel den von Eric Lippert. Dies entspricht der Reaktion, die erfahrene Ingenieure auf solche Optimierungen im Allgemeinen erhalten. Wenn Sie im Produktionscode binäre Verschiebungen als umfassendes Mittel zum Multiplizieren und Dividieren verwenden, werden die Leute höchstwahrscheinlich an Ihrem Code zusammenzucken und eine gewisse emotionale Reaktion haben ("Ich habe gehört, dass diese unsinnige Behauptung über JavaScript um Himmels willen") Es mag für unerfahrene Programmierer keinen Sinn ergeben, wenn sie die Gründe für diese Reaktionen nicht besser verstehen.
Diese Gründe sind in erster Linie eine Kombination aus verminderter Lesbarkeit und Sinnlosigkeit einer solchen Optimierung, wie Sie vielleicht bereits beim Vergleich der relativen Leistung herausgefunden haben. Ich glaube jedoch nicht, dass die Menschen so stark reagieren würden, wenn die Substitution der Verschiebung durch die Multiplikation das einzige Beispiel für solche Optimierungen wäre. Fragen wie Ihre tauchen häufig in verschiedenen Formen und Zusammenhängen auf. Ich denke, dass mehr leitende Ingenieure tatsächlich so stark reagieren, zumindest manchmal, dass das Potenzial für ein viel breiteres Schadensspektrum besteht, wenn Menschen solche Mikrooptimierungen großzügig in der gesamten Codebasis einsetzen. Wenn Sie in einem Unternehmen wie Microsoft auf einer großen Codebasis arbeiten, verbringen Sie viel Zeit damit, den Quellcode anderer Ingenieure zu lesen, oder versuchen, bestimmten Code darin zu finden. Es kann sogar Ihr eigener Code sein, den Sie in ein paar Jahren zu verstehen versuchen, insbesondere zu einigen der ungünstigsten Zeiten, wenn Sie beispielsweise nach einem Anruf auf dem Pager einen Produktionsausfall beheben müssen Dienst am Freitagabend, um eine Nacht voller Spaß mit Freunden zu verbringen ... Wenn Sie so viel Zeit mit dem Lesen von Code verbringen, werden Sie es zu schätzen wissen, dass er so gut wie möglich lesbar ist. Stellen Sie sich vor, Sie lesen Ihren Lieblingsroman, aber der Verlag hat beschlossen, eine neue Ausgabe zu veröffentlichen, in der abbrv verwendet wird. alles über die plc bcs dein thnk es svs spc. Das ist vergleichbar mit den Reaktionen, die andere Ingenieure auf Ihren Code haben, wenn Sie sie mit solchen Optimierungen bestreuen. Wie andere Antworten gezeigt haben, ist es besser, klar zu sagen, was Sie meinen,
Selbst in diesen Umgebungen werden Sie möglicherweise eine Interviewfrage lösen, bei der Sie diese oder eine andere Entsprechung kennen müssen. Es ist nicht schlecht, sie zu kennen, und ein guter Ingenieur wäre sich des arithmetischen Effekts der binären Verschiebung bewusst. Beachten Sie, dass ich nicht gesagt habe, dass dies ein guter Ingenieur ist, aber dass ein guter Ingenieur es meiner Meinung nach wissen würde. Insbesondere finden Sie möglicherweise noch einen Manager, in der Regel gegen Ende Ihrer Interviewschleife, der Sie breit angrinst, um Ihnen diesen cleveren technischen "Trick" in einer Codierungsfrage zu enthüllen und zu beweisen, dass er / sie es ist Auch war oder ist er einer der versierten Ingenieure und nicht "nur" ein Manager. Versuchen Sie in solchen Situationen, beeindruckt auszusehen, und danken Sie ihm für das aufschlussreiche Interview.
Warum haben Sie in C keinen Geschwindigkeitsunterschied festgestellt? Die wahrscheinlichste Antwort ist, dass beide den gleichen Assembler-Code ergeben haben:
int shift(int i) { return i << 2; }
int multiply(int i) { return i * 2; }
Kann beides zusammenstellen in
shift(int):
lea eax, [0+rdi*4]
ret
Auf GCC ohne Optimierungen, dh unter Verwendung des Flags "-O0", erhalten Sie möglicherweise Folgendes:
shift(int):
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], edi
mov eax, DWORD PTR [rbp-4]
sal eax, 2
pop rbp
ret
multiply(int):
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], edi
mov eax, DWORD PTR [rbp-4]
add eax, eax
pop rbp
ret
Wie Sie sehen können, bedeutet die Übergabe von "-O0" an GCC nicht, dass es nicht klug ist, welche Art von Code erzeugt wird. Beachten Sie insbesondere, dass der Compiler auch in diesem Fall die Verwendung eines Multiplikationsbefehls vermieden hat. Sie können das gleiche Experiment mit Verschiebungen durch andere Zahlen und sogar Multiplikationen durch Zahlen, die keine Zweierpotenzen sind, wiederholen. Wahrscheinlich sehen Sie auf Ihrer Plattform eine Kombination aus Verschiebungen und Hinzufügungen, jedoch keine Multiplikationen. Es scheint ein Zufall zu sein, dass der Compiler in all diesen Fällen anscheinend die Verwendung von Multiplikationen vermeidet, wenn Multiplikationen und Verschiebungen tatsächlich die gleichen Kosten verursachen, nicht wahr? Aber ich will keine Vermutung als Beweis liefern, also lasst uns weitermachen.
Sie könnten Ihren Test mit dem obigen Code wiederholen und sehen, ob Sie jetzt einen Geschwindigkeitsunterschied bemerken. Selbst dann testen Sie nicht Shift versus Multiplikation, wie Sie an der fehlenden Multiplikation erkennen können, sondern den Code, der von GCC mit einer bestimmten Menge von Flags für die C-Operationen Shift und Multiplikation in einer bestimmten Instanz generiert wurde . In einem anderen Test können Sie also den Assembly-Code manuell bearbeiten und stattdessen eine "imul" -Anweisung im Code für die "multiplizieren" -Methode verwenden.
Wenn Sie einige dieser intelligenten Elemente des Compilers beseitigen möchten, können Sie eine allgemeinere Verschiebungs- und Multiplikationsmethode definieren, die am Ende ungefähr so aussieht:
int shift(int i, int j) { return i << j; }
int multiply(int i, int j) { return i * j; }
Welche möglicherweise den folgenden Assemblycode ergeben:
shift(int, int):
mov eax, edi
mov ecx, esi
sal eax, cl
ret
multiply(int, int):
mov eax, edi
imul eax, esi
ret
Hier haben wir endlich, selbst auf der höchsten Optimierungsstufe von GCC 4.9, den Ausdruck in Montageanweisungen, den Sie erwartet haben, als Sie anfänglich mit Ihrem Test begannen. Ich denke, dass an sich eine wichtige Lektion bei der Leistungsoptimierung sein kann. Wir können den Unterschied sehen, den es gemacht hat, um Variablen für konkrete Konstanten in unserem Code zu ersetzen, in Bezug auf die Intelligenz, die der Compiler anwenden kann. Mikrooptimierungen wie die Umschalt-Multiplikations-Ersetzung sind einige sehr einfache Optimierungen, die ein Compiler normalerweise leicht selbst durchführen kann. Andere Optimierungen, die sich wesentlich stärker auf die Leistung auswirken, erfordern ein Verständnis der Absicht des CodesDas ist für den Compiler oft nicht zugänglich oder kann nur von einer Heuristik erraten werden. Hier kommen Sie als Softwareentwickler ins Spiel, und in der Regel müssen Multiplikationen nicht durch Verschiebungen ersetzt werden. Dabei geht es um Faktoren wie die Vermeidung eines redundanten Aufrufs eines Dienstes, der E / A erzeugt und einen Prozess blockieren kann. Wenn Sie zu Ihrer Festplatte gehen oder, Gott bewahre, zu einer entfernten Datenbank, um zusätzliche Daten zu erhalten, die Sie möglicherweise aus dem bereits vorhandenen Speicher erhalten haben, überwiegt die Zeit, die Sie für das Warten aufwenden, die Ausführung von einer Million Anweisungen. Nun, ich denke, wir sind ein bisschen weit von Ihrer ursprünglichen Frage entfernt, aber ich denke, wir weisen einen Fragesteller darauf hin, insbesondere wenn wir annehmen, dass jemand gerade erst anfängt, die Übersetzung und Ausführung von Code zu verstehen.
Also, welcher wird schneller sein? Ich denke, es ist ein guter Ansatz, den Sie gewählt haben, um den Leistungsunterschied tatsächlich zu testen. Im Allgemeinen ist es leicht, von der Laufzeitleistung einiger Codeänderungen überrascht zu werden. Es gibt viele Techniken, die moderne Prozessoren anwenden, und die Interaktion zwischen Software kann auch komplex sein. Selbst wenn Sie für eine bestimmte Änderung in einer Situation vorteilhafte Leistungsergebnisse erzielen sollten, ist es meines Erachtens gefährlich zu folgern, dass diese Art der Änderung immer zu Leistungsvorteilen führt. Ich halte es für gefährlich, solche Tests einmal durchzuführen und sage: "Okay, jetzt weiß ich, welcher schneller ist!" und wenden Sie dann wahllos dieselbe Optimierung auf den Produktionscode an, ohne Ihre Messungen zu wiederholen.
Was ist, wenn die Verschiebung schneller ist als die Multiplikation? Es gibt sicherlich Hinweise, warum dies zutreffen würde. Wie Sie oben sehen können, scheint GCC (auch ohne Optimierung) der Meinung zu sein, dass die Vermeidung einer direkten Multiplikation zugunsten anderer Anweisungen eine gute Idee ist. Das Referenzhandbuch zur Optimierung der Intel 64- und IA-32-Architekturen gibt Ihnen einen Überblick über die relativen Kosten von CPU-Anweisungen. Eine weitere Ressource, die sich mehr auf die Anweisungswartezeit und den Durchsatz konzentriert, ist http://www.agner.org/optimize/instruction_tables.pdf. Beachten Sie, dass sie kein guter Indikator für die absolute Laufzeit sind, sondern für die relative Ausführung von Anweisungen. In einer engen Schleife sollte, während Ihr Test simuliert, die Metrik des "Durchsatzes" am relevantesten sein. Dies ist die Anzahl der Zyklen, für die eine Ausführungseinheit normalerweise gebunden ist, wenn ein bestimmter Befehl ausgeführt wird.
Was ist, wenn die Verschiebung NICHT schneller ist als die Multiplikation? Wie bereits erwähnt, können moderne Architekturen sehr komplex sein, und Dinge wie Verzweigungsvorhersage, Caching, Pipelining und parallele Ausführungseinheiten können es schwierig machen, die relative Leistung von zwei logisch äquivalenten Codeteilen zuweilen vorherzusagen. Ich möchte das wirklich betonen, denn hier bin ich mit den meisten Antworten auf solche Fragen nicht zufrieden, und das Lager der Leute sagt geradezu, dass es (nicht mehr) einfach nicht wahr ist, dass das Schalten schneller ist als das Multiplizieren.
Nein, soweit mir bekannt ist, haben wir in den 1970er Jahren keine geheime Technik-Sauce erfunden oder wann immer die Kostenunterschiede zwischen einer Multiplikationseinheit und einem Bit-Shifter plötzlich aufgehoben werden sollten. Eine allgemeine Multiplikation in Bezug auf logische Gatter und sicherlich in Bezug auf logische Operationen ist in vielen Szenarien und auf vielen Architekturen immer noch komplexer als eine Verschiebung mit einem Barrel-Shifter. Wie sich dies in der Gesamtlaufzeit auf einem Desktop-Computer niederschlägt, ist möglicherweise etwas undurchsichtig. Ich weiß nicht genau, wie sie in bestimmten Prozessoren implementiert sind, aber hier ist eine Erklärung für eine Multiplikation: Ist die ganzzahlige Multiplikation wirklich die gleiche Geschwindigkeit wie die Addition auf einer modernen CPU?
Während hier eine Erklärung eines Barrel Shifter . Die Dokumente, auf die ich im vorigen Absatz verwiesen habe, geben einen anderen Überblick über die relativen Betriebskosten nach Proxy-Anweisungen der CPU. Die Ingenieure bei Intel scheinen häufig ähnliche Fragen zu haben: Intel Developer Zone Foren Taktzyklen für die ganzzahlige Multiplikation und Addition in Core-2-Duo-Prozessor
Ja, in den meisten realen Szenarien und mit ziemlicher Sicherheit in JavaScript ist der Versuch, diese Äquivalenz aus Gründen der Leistung auszunutzen, wahrscheinlich ein vergebliches Unterfangen. Auch wenn wir die Verwendung von Multiplikationsanweisungen erzwungen haben und dann keinen Laufzeitunterschied festgestellt haben, liegt dies eher an der Art der von uns verwendeten Kostenmetrik, genauer gesagt, und nicht daran, dass es keinen Kostenunterschied gibt. End-to-End-Laufzeit ist eine Metrik und wenn es die einzige ist, die uns interessiert, ist alles in Ordnung. Das heißt aber nicht, dass alle Kostenunterschiede zwischen Multiplikation und Verschiebung einfach verschwunden sind. Und ich denke, es ist sicherlich keine gute Idee, diese Idee implizit oder auf andere Weise einem Fragesteller zu übermitteln, der offensichtlich gerade erst anfängt, eine Vorstellung von den Faktoren zu bekommen, die mit der Laufzeit und den Kosten des modernen Codes zusammenhängen. Beim Engineering geht es immer um Kompromisse. Die Untersuchung und Erklärung, welche Kompromisse moderne Prozessoren gemacht haben, um die Ausführungszeit zu zeigen, die wir als Benutzer am Ende sehen, kann eine differenziertere Antwort liefern. Und ich denke, eine differenziertere Antwort als "das ist einfach nicht mehr wahr" ist gerechtfertigt, wenn weniger Ingenieure die Lesbarkeit von mikrooptimiertem Code auslöschen wollen, weil ein allgemeineres Verständnis der Art solcher "Optimierungen" erforderlich ist Finden Sie die verschiedenen, unterschiedlichen Inkarnationen, als sich lediglich auf bestimmte veraltete Instanzen zu beziehen.