it-swarm.com.de

Ist es empfehlenswert, die Division nach Möglichkeit durch Multiplikation zu ersetzen?

Wann immer 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:

  1. Durch Null teilen

  2. Überlauf, wenn oldValue sehr klein ist

Ist das richtig? Gibt es ein Problem für diese Gewohnheit?

72
ocomfd

Zwei häufig zu berücksichtigende Fälle:

Ganzzahlige Arithmetik

Wenn Sie eine Ganzzahlarithmetik verwenden (die abgeschnitten wird), erhalten Sie natürlich ein anderes Ergebnis. Hier ist ein kleines Beispiel in C #:

public static void TestIntegerArithmetic()
{
    int newValue = 101;
    int oldValue = 10;
    int SOME_CONSTANT = 10;

    if(newValue / oldValue > SOME_CONSTANT)
    {
        Console.WriteLine("First comparison says it's bigger.");
    }
    else
    {
        Console.WriteLine("First comparison says it's not bigger.");
    }

    if(newValue > oldValue * SOME_CONSTANT)
    {
        Console.WriteLine("Second comparison says it's bigger.");
    }
    else
    {
        Console.WriteLine("Second comparison says it's not bigger.");
    }
}

Ausgabe:

First comparison says it's not bigger.
Second comparison says it's bigger.

Gleitkomma-Arithmetik

Abgesehen von der Tatsache, dass die Division ein anderes Ergebnis liefern kann, wenn sie durch Null dividiert wird (sie erzeugt eine Ausnahme, während die Multiplikation dies nicht tut), kann sie auch zu leicht unterschiedlichen Rundungsfehlern und einem anderen Ergebnis führen. Einfaches Beispiel in C #:

public static void TestFloatingPoint()
{
    double newValue = 1;
    double oldValue = 3;
    double SOME_CONSTANT = 0.33333333333333335;

    if(newValue / oldValue >= SOME_CONSTANT)
    {
        Console.WriteLine("First comparison says it's bigger.");
    }
    else
    {
        Console.WriteLine("First comparison says it's not bigger.");
    }

    if(newValue >= oldValue * SOME_CONSTANT)
    {
        Console.WriteLine("Second comparison says it's bigger.");
    }
    else
    {
        Console.WriteLine("Second comparison says it's not bigger.");
    }
}

Ausgabe:

First comparison says it's not bigger.
Second comparison says it's bigger.

Falls Sie mir nicht glauben, hier ist eine Geige , die Sie ausführen und selbst sehen können.

Andere Sprachen können unterschiedlich sein; Beachten Sie jedoch, dass C # wie viele andere Sprachen eine Gleitkommabibliothek IEEE-Standard (IEEE 754) implementiert, sodass Sie in anderen standardisierten Laufzeiten dieselben Ergebnisse erzielen sollten.

Fazit

Wenn Sie arbeiten greenfield , sind Sie wahrscheinlich in Ordnung.

Wenn Sie an Legacy-Code arbeiten und die Anwendung eine finanzielle oder andere sensible Anwendung ist, die arithmetisch arbeitet und konsistente Ergebnisse liefern muss, seien Sie beim Umschalten von Vorgängen sehr vorsichtig. Wenn Sie müssen, stellen Sie sicher, dass Sie Unit-Tests haben, die subtile Änderungen in der Arithmetik erkennen.

Wenn Sie nur Elemente in einem Array oder andere allgemeine Rechenfunktionen zählen, sind Sie wahrscheinlich in Ordnung. Ich bin mir jedoch nicht sicher, ob die Multiplikationsmethode Ihren Code klarer macht.

Wenn Sie einen Algorithmus für eine Spezifikation implementieren, würde ich überhaupt nichts ändern, nicht nur wegen des Problems der Rundungsfehler, sondern damit Entwickler den Code überprüfen und jeden Ausdruck wieder der Spezifikation zuordnen können, um sicherzustellen, dass keine Implementierung erfolgt Mängel.

73
John Wu

Ich mag Ihre Frage, da sie möglicherweise viele Ideen abdeckt. Insgesamt vermute ich, dass die Antwort es hängt davon ab ist, wahrscheinlich von den beteiligten Typen und dem möglichen Wertebereich in Ihrem speziellen Fall.

Mein anfänglicher Instinkt ist es, über das Stil nachzudenken, dh. Ihre neue Version ist für den Leser Ihres Codes weniger klar. Ich stelle mir vor, ich müsste ein oder zwei Sekunden (oder vielleicht 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, daher ist Ihre neue Version mit Kosten verbunden.

Sie haben Recht, dass die neue Version eine Division durch Null vermeidet. Sicherlich müssen Sie keinen Schutz hinzufügen (im Sinne von 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, d. H. In diesem Fall sollten Sie kein Ergebnis erzielen.

Der Schutz vor Überlauf ist umstritten. Wenn Sie wissen, dass newValue immer größer als oldValue ist, können Sie dieses Argument vielleicht vorbringen. Es kann jedoch Fälle geben, in denen (oldValue * SOME_CONSTANT) wird auch überlaufen. Ich sehe hier also nicht viel Gewinn.

Möglicherweise gibt es ein Argument dafür, dass Sie eine bessere Leistung erzielen, da die Multiplikation schneller sein kann als die Division (auf einigen Prozessoren). Es müsste jedoch viele Berechnungen wie diese geben, um einen signifikanten Gewinn zu erzielen, d. H. Vorsicht vor vorzeitiger Optimierung.

Wenn ich über all das nachdenke, denke ich im Allgemeinen nicht, dass mit Ihrer neuen Version im Vergleich zur alten Version viel gewonnen werden kann, insbesondere angesichts der geringeren Klarheit. Es kann jedoch bestimmte Fälle geben, in denen ein gewisser Nutzen besteht.

25
dave

Nein.

Ich würde das wahrscheinlich vorzeitige Optimierung im weitesten Sinne nennen, unabhängig davon, ob Sie für Leistung optimieren, wie sich der Ausdruck allgemein bezieht, oder irgendetwas sonst kann das optimiert werden, wie Kantenanzahl , Codezeilen oder noch allgemeiner Dinge wie "Design."

Die Implementierung dieser Art der Optimierung als Standardbetriebsverfahren gefährdet die Semantik Ihres Codes und verbirgt möglicherweise die Kanten . Die Edge-Fälle, die Sie für unbeaufsichtigt halten, müssen möglicherweise ohnehin explizit angesprochen werden . Und es ist unendlich einfacher, Probleme an verrauschten Kanten (die Ausnahmen auslösen) über solche zu debuggen, die stillschweigend fehlschlagen.

In einigen Fällen ist es sogar vorteilhaft, die Optimierung aus Gründen der Lesbarkeit, Klarheit oder Aussagekraft zu "de-optimieren". In den meisten Fällen werden Ihre Benutzer nicht bemerken, dass Sie einige Codezeilen oder CPU-Zyklen gespeichert haben, um die Behandlung von Edge-Fällen oder Ausnahmen zu vermeiden. Umständlicher oder stillschweigend fehlgeschlagener Code hingegen wirkt sich auf Personen aus - zumindest auf Ihre Mitarbeiter. (Und daher auch die Kosten für die Erstellung und Wartung der Software.)

Standardmäßig wird alles verwendet, was "natürlicher" und in Bezug auf die Domäne der Anwendung und das spezifische Problem lesbar ist. Halten Sie es einfach, explizit und idiomatisch. Optimieren Sie nach Bedarf, um signifikante Gewinne zu erzielen oder um einen legitimen Usability-Schwellenwert zu erreichen.

Beachten Sie auch: Compiler oft Division optimieren für Sie sowieso - wenn es sicher ist um dies zu tun.

22
svidgen

Verwenden Sie einen, der weniger fehlerhaft und logischer ist.

Normalerweise, Division durch eine Variable ist sowieso 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:

Division gut:

if ((ptr2 - ptr1) >= n / 3)  // good: check if length of subarray is at least n/3
    ...

Multiplikation schlecht:

if ((ptr2 - ptr1) * 3 >= n)  // bad: confusing!! what is the intention of this code?
    ...

Multiplikation gut:

if (j - i >= 2 * min_length)  // good: obviously checking for a minimum length
    ...

Division schlecht:

if ((j - i) / 2 >= min_length)  // bad: confusing!! what is the intention of this code?
    ...

Multiplikation gut:

if (new_length >= old_length * 1.5)  // good: is the new size at least 50% bigger?
    ...

Division schlecht:

if (new_length / old_length >= 2)  // bad: BUGGY!! will fail if old_length = 0!
    ...
13
user541686

irgendetwas "wann immer möglich" zu tun ist sehr selten eine gute Idee.

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 zu findenden 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überprüfungen sind Ihre Freunde.

3
gnasher729

In Bezug auf die Lesbarkeit des Codes denke ich, dass die Multiplikation tatsächlich mehr ist. in einigen Fällen lesbar. Wenn Sie beispielsweise überprüfen müssen, ob newValue um 5 Prozent oder mehr über oldValue gestiegen ist, dann 1.05 * oldValue ist ein Schwellenwert, gegen den newValue getestet werden soll, und es ist natürlich zu schreiben

    if (newValue >= 1.05 * oldValue)

Aber Vorsicht 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 von Ihnen berücksichtigten Bedingungen sind gleichwertig, wenn garantiert ist, dass oldValue nicht negativ ist. Angenommen, newValue ist tatsächlich -13,5 und oldValue ist -10,1. Dann

newValue/oldValue >= 1.05

ergibt true, aber

newValue >= 1.05 * oldValue

ergibt false.

1
David K

Beachten Sie das berühmte Papier Division durch invariante Ganzzahlen mit Multiplikation .

Der Compiler führt tatsächlich eine Multiplikation durch, wenn die Ganzzahl invariant ist! Keine Abteilung. Dies geschieht auch bei Nicht-Potenz von 2 Werten. Potenzen von 2 Teilungen 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 der Optimierung sicher, dass Sie wirklich einen echten Engpass optimieren und dass die Korrektheit nicht beeinträchtigt wird. Achten Sie auf einen ganzzahligen Überlauf.

Ich interessiere mich für die Mikrooptimierung, daher 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 Division; Sie müssen eine Funktion zum Teilen aufrufen, es gibt keine Divisionsanweisung in ARM.

Außerdem wird bei 32-Bit-Architekturen die 64-Bit-Division nicht optimiert, wie I herausgefunden .

1
juhist

Wenn Sie Punkt 2 aufgreifen, wird tatsächlich ein Überlauf für ein sehr kleines oldValue verhindert. Wenn jedoch SOME_CONSTANT Auch sehr klein ist, führt Ihre alternative Methode zu einem Unterlauf, bei dem der Wert nicht genau dargestellt werden kann.

Und umgekehrt, was passiert, wenn oldValue sehr groß ist? Sie haben die gleichen Probleme, genau umgekehrt.

Wenn Sie das Risiko eines Überlaufs/Unterlaufs vermeiden (oder minimieren) möchten, überprüfen Sie am besten, ob newValue in seiner Größe oldValue oder SOME_CONSTANT Am nächsten kommt. Sie können dann entweder die entsprechende Divisionsoperation auswählen

    if(newValue / oldValue >= SOME_CONSTANT)

oder

    if(newValue / SOME_CONSTANT >= oldValue)

und das Ergebnis wird am genauesten sein.

Für die Division durch Null ist dies meiner Erfahrung nach fast nie angemessen, um in der Mathematik "gelöst" zu werden. Wenn Sie bei Ihren fortlaufenden Überprüfungen eine Division durch Null haben, 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 Null ist fast immer der richtige Schritt. (Beachten Sie, dass ich hier "fast" sage, weil ich nicht behaupte, unfehlbar zu sein. Ich stelle nur fest, dass ich mich nicht daran erinnere, in 20 Jahren des Schreibens eingebetteter Software einen guten Grund dafür gesehen zu haben, und gehe weiter .)

Wenn Sie jedoch ein echtes Risiko für Über-/Unterlauf in Ihrer Anwendung haben, ist dies wahrscheinlich nicht die richtige Lösung. Wahrscheinlicher ist es, dass Sie im Allgemeinen die numerische Stabilität Ihres Algorithmus überprüfen oder einfach zu einer Darstellung mit höherer Genauigkeit wechseln.

Und wenn Sie kein nachgewiesenes Risiko eines Überlaufs/Unterlaufs haben, machen Sie sich um nichts Sorgen. Das bedeutet, dass Sie wörtlich 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 Leute überprüft, würde ich persönlich nichts weniger akzeptieren, wenn ich jemanden treffen würde, der sich darüber zusätzliche Mühe gibt. Dies ist das Gegenteil von vorzeitiger Optimierung, hat aber im Allgemeinen dieselbe Grundursache - Besessenheit mit Details, die keinen funktionalen Unterschied macht.

1
Graham

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 stehen ausgefeiltere Techniken zur Verfügung. 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 recht komplex. Im Allgemeinen erfordern sie mehr Taktzyklen pro Bit.

0
Ishan Shah

Kapselung der bedingten Arithmetik in sinnvolle Methoden und Eigenschaften. Eine gute Benennung sagt Ihnen nicht nur, was "A/B" bedeutet, Parameterprüfung und Fehlerbehandlung können sich auch dort ordentlich verstecken.

Da diese Methoden zu einer komplexeren Logik zusammengesetzt sind, bleibt die Komplexität extrinsisch sehr überschaubar.

Ich würde sagen, Multiplikationssubstitution scheint eine vernünftige Lösung zu sein, da das Problem schlecht definiert ist.

0
radarbob