Sie können verwenden <<
, >>
um Zahlen in Python zu multiplizieren und zu teilen, wenn ich sie zeitlich festlege. Ich finde, dass die binäre Verschiebung 10x schneller ist als das Teilen oder Multiplizieren der regulären Methode.
Warum ist <<
und >>
viel schneller als *
und /
?
Was sind die hinter den Kulissen Prozesse gehen zu machen *
und /
so langsam?
operators
bitwise-operators
Crizly
quelle
quelle
Antworten:
Schauen wir uns zwei kleine C-Programme an, die eine kleine Verschiebung und eine Teilung bewirken.
Diese werden dann jeweils kompiliert, um
gcc -S
zu sehen, wie die eigentliche Baugruppe aussehen wird.Mit der Bitverschiebungsversion vom Aufruf bis
atoi
zur Rückkehr:Während die Divide-Version:
Wenn man sich das nur ansieht, gibt es in der Divide-Version im Vergleich zur Bitverschiebung mehrere Anweisungen mehr.
Der Schlüssel ist, was machen sie?
In der Bitverschiebungsversion ist der Schlüsselbefehl
shll $2, %eax
eine logische Verschiebung nach links - es gibt die Teilung, und alles andere bewegt nur Werte.In der Divide-Version sehen Sie das
idivl %r8d
- aber genau darüber befindet sich eincltd
(Long in Double konvertieren) und eine zusätzliche Logik rund um das Verschütten und Nachladen. Diese zusätzliche Arbeit, in dem Wissen, dass es sich eher um eine Mathematik als um Bits handelt, ist häufig erforderlich, um verschiedene Fehler zu vermeiden, die nur durch Bit-Mathematik auftreten können.Machen wir eine schnelle Multiplikation:
Anstatt all dies durchzugehen, gibt es eine andere Zeile:
Hier konnte der Compiler feststellen, dass die Mathematik mit einer Verschiebung durchgeführt werden kann, jedoch anstelle einer logischen Verschiebung eine arithmetische Verschiebung. Der Unterschied zwischen diesen wäre offensichtlich, wenn wir diese ausführen würden -
sarl
bewahrt das Zeichen. Also-2 * 4 = -8
dasshll
tut das nicht.Schauen wir uns das in einem kurzen Perl-Skript an:
Ausgabe:
Ähm ...
-4 << 2
ist18446744073709551600
nicht genau das, was Sie wahrscheinlich erwarten, wenn Sie sich mit Multiplikation und Division befassen. Es ist richtig, aber es ist keine ganzzahlige Multiplikation.Und seien Sie daher vorsichtig bei vorzeitiger Optimierung. Lassen Sie den Compiler für Sie optimieren - er weiß, was Sie wirklich versuchen, und wird es wahrscheinlich mit weniger Fehlern besser machen.
quelle
<< 2
mit* 4
und>> 2
mit/ 4
zu koppeln, um die Verschiebungsrichtungen in jedem Beispiel gleich zu halten.Die vorhandenen Antworten haben die Hardware-Seite der Dinge nicht wirklich angesprochen, daher hier ein wenig zu diesem Aspekt. Die übliche Weisheit ist, dass Multiplikation und Division viel langsamer sind als Verschiebung, aber die heutige Geschichte ist nuancierter.
Zum Beispiel ist es sicher richtig, dass die Multiplikation eine komplexere Operation ist, die in Hardware implementiert werden muss, aber sie muss nicht immer langsamer sein . Wie sich herausstellt,
add
ist die Implementierung auch wesentlich komplexer alsxor
(oder im Allgemeinen jede bitweise Operation), aberadd
(undsub
) erhalten normalerweise genug Transistoren für ihre Operation, die am Ende genauso schnell sind wie die bitweisen Operatoren. Sie können also nicht nur die Komplexität der Hardware-Implementierung als Leitfaden für die Geschwindigkeit betrachten.Schauen wir uns also die Verschiebung im Vergleich zu den "vollständigen" Operatoren wie Multiplikation und Verschiebung genauer an.
Verschiebung
Auf fast jeder Hardware ist das Verschieben um einen konstanten Betrag (dh einen Betrag, den der Compiler zur Kompilierungszeit bestimmen kann) schnell . Insbesondere geschieht dies normalerweise mit einer Latenz von einem einzelnen Zyklus und mit einem Durchsatz von 1 pro Zyklus oder besser. Auf einigen Hardwarekomponenten (z. B. einigen Intel- und ARM-Chips) können bestimmte Verschiebungen durch eine Konstante sogar "frei" sein, da sie in einen anderen Befehl integriert werden können (
lea
bei Intel die besonderen Verschiebungsfähigkeiten der ersten Quelle in ARM).Die Verschiebung um einen variablen Betrag ist eher eine Grauzone. Bei älterer Hardware war dies manchmal sehr langsam, und die Geschwindigkeit änderte sich von Generation zu Generation. Zum Beispiel war bei der ersten Veröffentlichung von Intels P4 das Schalten um einen variablen Betrag notorisch langsam - was Zeit erfordert, die proportional zum Verschiebungsbetrag ist! Auf dieser Plattform könnte die Verwendung von Multiplikationen als Ersatz für Schichten rentabel sein (dh die Welt ist auf den Kopf gestellt worden). Bei früheren Intel-Chips sowie nachfolgenden Generationen war eine Verschiebung um einen variablen Betrag nicht so schmerzhaft.
Bei aktuellen Intel-Chips ist die Verschiebung um einen variablen Betrag nicht besonders schnell, aber auch nicht schrecklich. Die x86-Architektur ist in Bezug auf variable Verschiebungen stark eingeschränkt, da sie die Operation auf ungewöhnliche Weise definiert: Verschiebungsbeträge von 0 ändern die Bedingungsflags nicht, alle anderen Verschiebungen jedoch. Dies verhindert das effiziente Umbenennen des Flags-Registers, da nicht bestimmt werden kann, bis die Verschiebung ausgeführt wird, ob nachfolgende Anweisungen die von der Verschiebung geschriebenen Bedingungscodes oder eine vorherige Anweisung lesen sollen. Darüber hinaus schreiben Verschiebungen nur in einen Teil des Flags-Registers, was zu einem teilweisen Stillstand der Flags führen kann.
Das Ergebnis ist dann, dass bei neueren Intel-Architekturen eine Verschiebung um einen variablen Betrag drei "Mikrooperationen" erfordert, während die meisten anderen einfachen Operationen (Addieren, bitweise Operationen, sogar Multiplizieren) nur 1 benötigen. Solche Verschiebungen können höchstens einmal alle 2 Zyklen ausgeführt werden .
Multiplikation
Der Trend bei moderner Desktop- und Laptop- Hardware geht dahin, die Multiplikation zu einem schnellen Vorgang zu machen. Bei neueren Intel- und AMD-Chips kann tatsächlich eine Multiplikation pro Zyklus ausgegeben werden (wir nennen dies gegenseitigen Durchsatz ). Die Latenz einer Multiplikation beträgt jedoch 3 Zyklen. Das bedeutet, dass Sie das Ergebnis einer bestimmten Multiplikation 3 Zyklen nach dem Start erhalten, aber Sie können in jedem Zyklus eine neue Multiplikation starten. Welcher Wert (1 Zyklus oder 3 Zyklen) wichtiger ist, hängt von der Struktur Ihres Algorithmus ab. Wenn die Multiplikation Teil einer kritischen Abhängigkeitskette ist, ist die Latenz wichtig. Wenn nicht, können der wechselseitige Durchsatz oder andere Faktoren wichtiger sein.
Der Schlüssel zum Erfolg ist, dass bei modernen Laptop- Chips (oder besser) die Multiplikation eine schnelle Operation ist und wahrscheinlich schneller als die 3- oder 4-Befehlssequenz ist, die ein Compiler ausgeben würde, um die richtige Rundung für kraftreduzierte Verschiebungen zu erzielen. Bei variablen Verschiebungen würde bei Intel aufgrund der oben genannten Probleme im Allgemeinen auch die Multiplikation bevorzugt.
Auf Plattformen mit kleinerem Formfaktor kann die Multiplikation immer noch langsamer sein, da der Aufbau eines vollständigen und schnellen 32-Bit- oder insbesondere 64-Bit-Multiplikators viel Transistoren und Leistung erfordert. Wenn jemand Einzelheiten über die Leistung der Multiplikation auf neueren mobilen Chips angeben kann, wäre er sehr dankbar.
Teilen
Teilen ist sowohl in Bezug auf die Hardware als auch in Bezug auf die Multiplikation eine komplexere Operation und kommt im tatsächlichen Code auch viel seltener vor - was bedeutet, dass ihm wahrscheinlich weniger Ressourcen zugewiesen werden. Der Trend bei modernen Chips geht immer noch zu schnelleren Teilern, aber selbst moderne Spitzen-Chips benötigen 10 bis 40 Zyklen, um eine Teilung durchzuführen, und sie sind nur teilweise per Pipeline. Im Allgemeinen sind 64-Bit-Teilungen sogar langsamer als 32-Bit-Teilungen. Im Gegensatz zu den meisten anderen Operationen kann die Division abhängig von den Argumenten eine variable Anzahl von Zyklen dauern.
Vermeiden Sie Teilungen und ersetzen Sie sie durch Verschiebungen (oder lassen Sie den Compiler dies tun, aber Sie müssen möglicherweise die Baugruppe überprüfen), wenn Sie können!
quelle
BINARY_LSHIFT und BINARY_RSHIFT sind algorithmisch einfachere Prozesse als BINARY_MULTIPLY und BINARY_FLOOR_DIVIDE und benötigen möglicherweise weniger Taktzyklen. Das heißt, wenn Sie eine Binärzahl haben und um N bitverschieben müssen, müssen Sie nur die Ziffern über so viele Leerzeichen verschieben und durch Nullen ersetzen. Die binäre Multiplikation ist im Allgemeinen komplizierter , obwohl Techniken wie der Dadda-Multiplikator sie ziemlich schnell machen.
Zugegeben, ein optimierender Compiler kann Fälle erkennen, in denen Sie mit Zweierpotenzen multiplizieren / dividieren und durch die entsprechende Links- / Rechtsverschiebung ersetzen. Wenn man sich den zerlegten Bytecode anschaut, tut Python dies anscheinend nicht:
Auf meinem Prozessor habe ich jedoch festgestellt, dass Multiplikation und Links- / Rechtsverschiebung ein ähnliches Timing haben und die Bodenteilung (um eine Zweierpotenz) etwa 25% langsamer ist:
quelle