Wenn ich eine Division benötige, zum Beispiel eine Bedingungsprüfung, möchte ich den Ausdruck der Division in Multiplikation umgestalten, zum Beispiel:
Originalfassung:
if(newValue / oldValue >= SOME_CONSTANT)
Neue Version:
if(newValue >= oldValue * SOME_CONSTANT)
Weil ich denke, es kann vermeiden:
Durch Null teilen
Überlauf, wenn
oldValue
sehr klein ist
Ist das richtig? Gibt es ein Problem für diese Angewohnheit?
coding-style
language-agnostic
math
ocomfd
quelle
quelle
oldValue >= 0
?Antworten:
Zwei häufig zu berücksichtigende Fälle:
Ganzzahlige Arithmetik
Wenn Sie eine Ganzzahl-Arithmetik verwenden (die abschneidet), erhalten Sie offensichtlich ein anderes Ergebnis. Hier ist ein kleines Beispiel in C #:
Ausgabe:
Gleitkomma-Arithmetik
Abgesehen von der Tatsache, dass eine Division ein anderes Ergebnis liefern kann, wenn sie durch Null dividiert (sie erzeugt eine Ausnahme, während eine Multiplikation dies nicht tut), kann sie auch zu leicht unterschiedlichen Rundungsfehlern und einem anderen Ergebnis führen. Einfaches Beispiel in C #:
Ausgabe:
Falls du mir nicht glaubst, hier ist eine Geige, die du ausführen und selbst sehen kannst.
Andere Sprachen können abweichen. Bedenken Sie jedoch, dass C # wie viele andere Sprachen eine Gleitkommabibliothek nach IEEE-Standard (IEEE 754) implementiert , sodass Sie bei anderen standardisierten Laufzeiten dieselben Ergebnisse erzielen sollten.
Fazit
Wenn Sie auf der grünen Wiese arbeiten , sind Sie wahrscheinlich in Ordnung.
Wenn Sie an altem Code arbeiten und es sich bei der Anwendung um eine finanzielle oder andere vertrauliche Anwendung handelt, die arithmetisch arbeitet und konsistente Ergebnisse liefern muss, gehen Sie beim Ändern von Vorgängen sehr vorsichtig vor. Wenn Sie müssen, stellen Sie sicher, dass Sie Komponententests haben, die subtile Änderungen in der Arithmetik erkennen.
Wenn Sie nur Dinge wie das Zählen von Elementen in einem Array oder andere allgemeine Berechnungsfunktionen ausführen, sind Sie wahrscheinlich in Ordnung. Ich bin nicht sicher, ob die Multiplikationsmethode Ihren Code klarer macht.
Wenn Sie einen Algorithmus für eine Spezifikation implementieren, würde ich nichts ändern, nicht nur wegen des Problems der Rundungsfehler, sondern damit Entwickler den Code überprüfen und jeden Ausdruck der Spezifikation zuordnen können, um sicherzustellen, dass keine Implementierung vorliegt Mängel.
quelle
Ich mag Ihre Frage, da sie möglicherweise viele Ideen abdeckt. Insgesamt denke ich, dass die Antwort davon abhängt , wahrscheinlich von den beteiligten Typen und dem möglichen Wertebereich in Ihrem speziellen Fall.
Mein anfänglicher Instinkt ist es, über den Stil nachzudenken , d. H. Ihre neue Version ist für den Leser Ihres Codes weniger klar. Ich stelle mir vor, ich müsste ein oder zwei Sekunden (oder länger) nachdenken, um die Absicht Ihrer neuen Version zu bestimmen, während Ihre alte Version sofort klar ist. Die Lesbarkeit ist ein wichtiges Attribut des Codes, sodass Ihre neue Version Kosten verursacht.
Sie haben Recht, dass die neue Version eine Division durch Null vermeidet. Sicherlich müssen Sie keine Schutzvorrichtung hinzufügen (in Anlehnung an
if (oldValue != 0)
). Aber macht das Sinn? Ihre alte Version spiegelt ein Verhältnis zwischen zwei Zahlen wider. Wenn der Divisor Null ist, ist Ihr Verhältnis undefiniert. Dies kann in Ihrer Situation sinnvoller sein, z. In diesem Fall sollten Sie kein Ergebnis erzielen.Überlaufschutz ist umstritten. Wenn Sie wissen, dass
newValue
das immer größer ist alsoldValue
, dann könnten Sie vielleicht dieses Argument vorbringen. Es kann jedoch Fälle geben, in denen(oldValue * SOME_CONSTANT)
auch ein Überlauf auftritt. Daher sehe ich hier nicht viel Gewinn.Möglicherweise gibt es ein Argument dafür, dass Sie eine bessere Leistung erzielen, da die Multiplikation schneller als die Division sein kann (auf einigen Prozessoren). Es müssten jedoch viele Berechnungen wie diese durchgeführt werden, um einen signifikanten Gewinn zu erzielen, d. H. Vorsicht vor vorzeitiger Optimierung.
Nach alledem denke ich, dass mit Ihrer neuen Version im Vergleich zur alten Version im Allgemeinen nicht viel gewonnen werden kann, insbesondere angesichts der geringeren Klarheit. In bestimmten Fällen kann es jedoch Vorteile geben.
quelle
Nein.
Ich würde diese vorzeitige Optimierung wahrscheinlich im weiteren Sinne als " vorzeitige Optimierung" bezeichnen , unabhängig davon, ob Sie für die Leistung optimieren , wie der Ausdruck allgemein sagt, oder für alles andere, was optimiert werden kann, wie z. B. die Kantenanzahl , Codezeilen oder Noch allgemeiner: Dinge wie "Design".
Die Implementierung dieser Art von Optimierung als Standardbetriebsverfahren gefährdet die Semantik Ihres Codes und verbirgt möglicherweise die Ränder. Die Randfälle, die Sie für unbemerkt beseitigen können, müssen möglicherweise ohnehin explizit behandelt werden . Und es ist unendlich einfacher, Probleme an verrauschten Rändern (die Ausnahmen auslösen) gegenüber solchen zu beheben, die stillschweigend fehlschlagen.
In einigen Fällen ist es sogar vorteilhaft, die Optimierung zu deaktivieren, um die Lesbarkeit, Klarheit oder Aussagekraft zu verbessern. In den meisten Fällen werden Ihre Benutzer nicht bemerken, dass Sie einige Codezeilen oder CPU-Zyklen gespeichert haben, um die Behandlung von Edge-Cases oder Ausnahmen zu vermeiden. Peinlich oder Code leise versagt, auf der anderen Seite, werden Auswirkungen auf Menschen - Ihre Mitarbeiter am allerwenigsten. (Und daher auch die Kosten für die Erstellung und Wartung der Software.)
Die Standardeinstellung ist "natürlich" und in Bezug auf die Domäne der Anwendung und das spezifische Problem lesbar. Halten Sie es einfach, explizit und idiomatisch. Optimieren Sie nach Bedarf, um signifikante Gewinne zu erzielen oder eine legitime Usability-Schwelle zu erreichen.
Beachten Sie auch: Compiler optimieren die Aufteilung oft ohnehin für Sie - wenn es sicher ist .
quelle
Verwenden Sie das, was weniger fehlerhaft und logischer ist.
Normalerweise ist die Division durch eine Variable ohnehin eine schlechte Idee, da der Divisor normalerweise Null sein kann.
Die Division durch eine Konstante hängt normalerweise nur von der logischen Bedeutung ab.
Hier einige Beispiele, um zu zeigen, dass dies von der Situation abhängt:
Abteilung gut:
Multiplikation schlecht:
Multiplikation gut:
Division schlecht:
Multiplikation gut:
Division schlecht:
quelle
(ptr2 - ptr1) * 3 >= n
das genauso einfach zu verstehen wie den Ausdruckptr2 - ptr1 >= n / 3
? Es bringt Ihr Gehirn nicht dazu, wieder aufzustehen und zu versuchen, die Bedeutung der Verdreifachung des Unterschieds zwischen zwei Zeigern zu entschlüsseln? Wenn es für Sie und Ihr Team wirklich offensichtlich ist, dann schätze ich, dass Sie mehr Macht haben. Ich muss nur in der langsamen Minderheit sein.n
und eine willkürliche Zahl 3 sind in beiden Fällen verwirrend, aber, ersetzt durch sinnvolle Namen, nein, ich finde keinen verwirrender als den anderen.Es ist sehr selten eine gute Idee , „wann immer möglich“ irgendetwas zu tun .
Ihre oberste Priorität sollte die Korrektheit sein, gefolgt von Lesbarkeit und Wartbarkeit. Das blinde Ersetzen der Division durch Multiplikation, wann immer dies möglich ist, schlägt in der Korrektheitsabteilung häufig fehl, manchmal nur in seltenen und daher schwer auffindbaren Fällen.
Tun Sie, was richtig und am besten lesbar ist. Wenn Sie solide Beweise dafür haben, dass das Schreiben von Code auf die am besten lesbare Weise ein Leistungsproblem verursacht, können Sie eine Änderung in Betracht ziehen. Pflege, Mathematik und Code-Reviews sind deine Freunde.
quelle
In Bezug auf die Lesbarkeit des Codes denke ich, dass die Multiplikation in einigen Fällen tatsächlich besser lesbar ist. Wenn Sie beispielsweise überprüfen müssen, ob
newValue
ein Wert von 5 Prozent oder mehr überschritten wurdeoldValue
,1.05 * oldValue
ist dies ein Schwellenwert, an dem getestet werden mussnewValue
, und das Schreiben ist natürlichAber hüten Sie sich vor negativen Zahlen, wenn Sie die Dinge auf diese Weise umgestalten (entweder die Division durch Multiplikation ersetzen oder die Multiplikation durch Division ersetzen). Die beiden Bedingungen, die Sie in Betracht gezogen haben, sind gleichwertig, wenn
oldValue
garantiert nicht negativ ist. Nehmen wir aber an,newValue
tatsächlich ist -13,5 und -10,1oldValue
. Dannergibt true , aber
wird zu false ausgewertet .
quelle
Beachten Sie das berühmte Paper Division by Invariant Integers using Multiplication .
Der Compiler multipliziert tatsächlich, wenn die Ganzzahl unveränderlich ist! Keine Division. Dies geschieht auch bei Nicht-Potenz von 2 Werten. Potenz von 2 Divisionen verwenden offensichtlich Bitverschiebungen und sind daher noch schneller.
Für nichtinvariante Ganzzahlen liegt es jedoch in Ihrer Verantwortung, den Code zu optimieren. Stellen Sie vor dem Optimieren sicher, dass Sie wirklich einen echten Engpass optimieren und dass die Richtigkeit nicht beeinträchtigt wird. Vorsicht vor Überlauf von ganzen Zahlen.
Da mir die Mikrooptimierung am Herzen liegt, würde ich mir wahrscheinlich die Optimierungsmöglichkeiten ansehen.
Denken Sie auch an die Architekturen, auf denen Ihr Code ausgeführt wird. Insbesondere ARM hat eine extrem langsame Teilung; Sie müssen eine Funktion aufrufen, um zu teilen. In ARM gibt es keine Divisionsanweisung.
Auf 32-Bit-Architekturen ist die 64-Bit-Aufteilung nicht optimiert, wie ich herausgefunden habe .
quelle
Wenn Sie Ihren Punkt 2 aufgreifen, wird ein Überlauf für einen sehr kleinen Teil tatsächlich verhindert
oldValue
. WennSOME_CONSTANT
es sich jedoch auch um eine sehr kleine Methode handelt, kommt es zu einem Unterlauf, bei dem der Wert nicht genau dargestellt werden kann.Und umgekehrt, was passiert, wenn
oldValue
es sehr groß ist? Sie haben die gleichen Probleme, genau umgekehrt.Wenn Sie vermeiden möchten (oder zu minimieren) , um das Risiko von Über- / Unterlauf ist der beste Weg , um zu überprüfen , ob
newValue
in der Größe am nächsten ist ,oldValue
oderSOME_CONSTANT
. Sie können dann entweder die entsprechende Divisionsoperation auswählenoder
und das Ergebnis wird am genauesten sein.
Nach meiner Erfahrung ist es für die Division durch Null so gut wie nie angebracht, in der Mathematik "gelöst" zu werden. Wenn Sie bei Ihren kontinuierlichen Überprüfungen eine Division durch Null haben, dann haben Sie mit ziemlicher Sicherheit eine Situation, die eine Analyse erfordert, und Berechnungen, die auf diesen Daten basieren, sind bedeutungslos. Eine explizite Prüfung durch das Teilen durch Null ist fast immer der richtige Schritt. (Beachten Sie, dass ich hier "fast" sage, weil ich nicht behaupte, unfehlbar zu sein. Ich werde nur bemerken, dass ich mich nicht daran erinnere, dass ich in 20 Jahren des Schreibens eingebetteter Software einen guten Grund dafür gesehen habe und gehe weiter .)
Wenn Sie jedoch ein echtes Überlauf- / Unterlaufrisiko in Ihrer Anwendung haben, ist dies wahrscheinlich nicht die richtige Lösung. Wahrscheinlicher ist, dass Sie im Allgemeinen die numerische Stabilität Ihres Algorithmus überprüfen oder einfach zu einer präziseren Darstellung wechseln sollten.
Und wenn Sie kein nachgewiesenes Risiko für Über- oder Unterlauf haben, machen Sie sich um nichts Sorgen. Das bedeutet, dass Sie buchstäblich beweisen müssen, dass Sie es brauchen, mit Zahlen in Kommentaren neben dem Code, die einem Betreuer erklären, warum es notwendig ist. Als leitender Ingenieur, der den Code anderer überprüft, würde ich persönlich nichts weniger akzeptieren, wenn ich jemandem begegnen würde, der diesbezüglich zusätzliche Anstrengungen unternimmt. Dies ist das Gegenteil von vorzeitiger Optimierung, hat jedoch im Allgemeinen die gleiche Ursache: Besessenheit mit Details, die keinen funktionalen Unterschied macht.
quelle
Verkapseln Sie die bedingte Arithmetik in sinnvolle Methoden und Eigenschaften. Gute Benennung sagt nicht nur, was "A / B" bedeutet , sondern auch Parameterprüfung und Fehlerbehandlung.
Da diese Methoden zu einer komplexeren Logik zusammengesetzt sind, bleibt die extrinsische Komplexität sehr überschaubar.
Ich würde sagen, Multiplikationssubstitution scheint eine vernünftige Lösung zu sein, da das Problem schlecht definiert ist.
quelle
Ich denke, es könnte keine gute Idee sein, Multiplikationen durch Divisionen zu ersetzen, da die ALU (Arithmetic-Logic Unit) der CPU Algorithmen ausführt, obwohl sie in Hardware implementiert sind. In neueren Prozessoren sind komplexere Techniken verfügbar. Im Allgemeinen bemühen sich Prozessoren, Bitpaaroperationen zu parallelisieren, um die erforderlichen Taktzyklen zu minimieren. Multiplikationsalgorithmen können sehr effektiv parallelisiert werden (obwohl mehr Transistoren erforderlich sind). Divisionsalgorithmen können nicht so effizient parallelisiert werden. Die effizientesten Divisionsalgorithmen sind ziemlich komplex. Im Allgemeinen erfordern sie mehr Taktzyklen pro Bit.
quelle