Führt der Vergleich der Gleichheit der Gleitkommazahlen auch dann zu einer Irreführung der Nachwuchsentwickler, wenn in meinem Fall kein Rundungsfehler auftritt?

31

Zum Beispiel möchte ich eine Liste von Schaltflächen von 0,0.5, ... 5 anzeigen, die für jede 0,5 springt. Ich benutze eine for-Schleife, um das zu tun, und habe eine andere Farbe auf der Schaltfläche STANDARD_LINE:

var MAX=5.0;
var DIFF=0.5
var STANDARD_LINE=1.5;

for(var i=0;i<=MAX;i=i+DIFF){
    button.text=i+'';
    if(i==STANDARD_LINE){
      button.color='red';
    }
}

In diesem Fall sollte es keine Rundungsfehler geben, da jeder Wert in IEEE 754 genau ist. Ich habe jedoch Probleme, ihn zu ändern, um einen Gleitkomma-Gleichheitsvergleich zu vermeiden:

var MAX=10;
var STANDARD_LINE=3;

for(var i=0;i<=MAX;i++){
    button.text=i/2.0+'';
    if(i==STANDARD_LINE/2.0){
      button.color='red';
    }
}

Einerseits ist der ursprüngliche Code einfacher und geht an mich weiter. Aber eines überlege ich mir: Führt i == STANDARD_LINE Junior-Teamkollegen in die Irre? Verbirgt sich die Tatsache, dass Gleitkommazahlen Rundungsfehler aufweisen können? Nach dem Lesen von Kommentaren aus diesem Beitrag:

https://stackoverflow.com/questions/33646148/is-hardcode-float-precise-if-it-can-be-represented-by-binary-format-in-ieee-754

Es scheint, dass es viele Entwickler gibt, die nicht wissen, dass einige Gleitkommazahlen genau sind. Sollte ich Gleichheitsvergleiche mit Gleitkommazahlen vermeiden, auch wenn sie in meinem Fall gültig sind? Oder bin ich überlegt?

ocomfd
quelle
23
Das Verhalten dieser beiden Codeauflistungen ist nicht äquivalent. 3 / 2.0 ist 1,5, iwird aber immer nur eine ganze Zahl in der zweiten Auflistung sein. Versuchen Sie, die zweite zu entfernen /2.0.
candied_orange
27
Wenn Sie unbedingt zwei FPs vergleichen müssen, um die Gleichheit zu gewährleisten (was nicht erforderlich ist, wie andere in ihren feinen Antworten betonten, da Sie einfach einen Schleifenzählervergleich mit ganzen Zahlen verwenden können), aber wenn Sie dies getan haben, sollte ein Kommentar ausreichen. Persönlich habe ich lange mit IEEE FP zusammengearbeitet und ich wäre immer noch verwirrt, wenn ich einen direkten SPFP-Vergleich gesehen hätte, ohne irgendeinen Kommentar oder irgendetwas. Es ist nur sehr heikler Code - jedenfalls immer einen Kommentar wert.
14
Unabhängig von Ihrer Wahl ist dies einer der Fälle, in denen ein Kommentar, der das Wie und Warum erläutert, unbedingt erforderlich ist. Ein späterer Entwickler wird die Feinheiten möglicherweise nicht einmal kommentarlos in Betracht ziehen, um ihn darauf aufmerksam zu machen. Außerdem bin ich sehr abgelenkt von der Tatsache, dass buttonsich an keiner Stelle in Ihrer Schleife etwas ändert. Wie wird auf die Liste der Schaltflächen zugegriffen? Über einen Index in ein Array oder einen anderen Mechanismus? Wenn es sich um einen Indexzugriff auf ein Array handelt, ist dies ein weiteres Argument für die Umstellung auf Ganzzahlen.
jpmc26 am
9
Schreiben Sie diesen Code. Bis jemand denkt, dass 0,6 eine bessere Schrittgröße wäre und diese Konstante einfach ändert.
Tofro
11
"... Junior-Entwickler irreführen" Sie werden auch Senior-Entwickler irreführen. Trotz der Menge an Überlegungen, die Sie angestellt haben, wird davon ausgegangen, dass Sie nicht wussten, was Sie getan haben, und es wird wahrscheinlich sowieso in die ganzzahlige Version geändert.
GrandOpener

Antworten:

116

Aufeinanderfolgende Gleitkommaoperationen würde ich immer vermeiden, es sei denn, das Modell, das ich berechne, erfordert sie. Gleitkomma-Arithmetik ist für die meisten nicht intuitiv und eine wichtige Fehlerquelle. Und die Fälle, in denen es Fehler verursacht, von denen zu unterscheiden, in denen dies nicht der Fall ist, ist eine noch subtilere Unterscheidung!

Daher ist die Verwendung von Floats als Schleifenzähler ein Fehler, der darauf wartet, dass er auftritt, und der mindestens einen dicken Hintergrundkommentar erfordert, der erklärt, warum es in Ordnung ist, hier 0,5 zu verwenden, und dass dies vom spezifischen numerischen Wert abhängt. Zu diesem Zeitpunkt ist es wahrscheinlich besser lesbar, den Code neu zu schreiben, um Fließkommazähler zu vermeiden. Und Lesbarkeit ist neben Korrektheit in der Hierarchie der beruflichen Anforderungen.

Kilian Foth
quelle
48
Ich mag "einen Defekt, der darauf wartet, passiert zu werden". Sicher, es könnte jetzt funktionieren , aber eine leichte Brise von jemandem, der vorbeigeht, wird es brechen.
AakashM
10
Angenommen, die Anforderungen ändern sich, sodass anstelle von 11 Tasten mit gleichem Abstand von 0 bis 5 mit der "Standardlinie" auf der vierten Taste 16 Tasten mit gleichem Abstand von 0 bis 5 mit der "Standardlinie" auf der sechsten Taste vorhanden sind Taste. Wer auch immer diesen Code von Ihnen geerbt hat, ändert sich von 0,5 zu 1,0 / 3,0 und von 1,5 zu 5,0 / 3,0. Was passiert dann?
David K
8
Ja, ich fühle mich unwohl mit der Idee, dass das Ändern einer scheinbar willkürlichen Zahl (so "normal" wie eine Zahl sein könnte) in eine andere willkürliche Zahl (die ebenfalls "normal" zu sein scheint ) tatsächlich einen Fehler verursacht.
Alexander - Reinstate Monica
7
@Alexander: Richtig, du brauchst einen Kommentar, der besagt DIFF must be an exactly-representable double that evenly divides STANDARD_LINE. Wenn Sie diesen Kommentar nicht schreiben möchten (und sich darauf verlassen möchten, dass alle zukünftigen Entwickler genug über IEEE754 binary64-Gleitkomma wissen, um ihn zu verstehen), schreiben Sie den Code nicht auf diese Weise. dh schreibe den Code nicht so. Vor allem, weil es wahrscheinlich nicht effizienter ist: FP-Addition hat eine höhere Latenz als Integer-Addition und ist eine schleifenabhängige Abhängigkeit. Compiler (auch JIT-Compiler?) Sind wahrscheinlich besser darin, Schleifen mit Ganzzahlzählern zu erstellen.
Peter Cordes
39

In der Regel sollten Schleifen so geschrieben werden, dass Sie überlegen, etwas n- mal zu tun . Wenn Sie Gleitkommaindizes verwenden, müssen Sie nicht mehr n- mal etwas tun, sondern müssen so lange ausgeführt werden, bis eine Bedingung erfüllt ist. Wenn dieser Zustand dem sehr ähnlich ist, den i<nso viele Programmierer erwarten, dann scheint der Code eine Sache zu tun, wenn er tatsächlich eine andere tut, was von Programmierern, die den Code überfliegen, leicht falsch interpretiert werden kann.

Es ist etwas subjektiv, aber meiner bescheidenen Meinung nach sollten Sie dies tun, wenn Sie eine Schleife neu schreiben können, um einen Ganzzahlindex zu verwenden, um eine feste Anzahl von Schleifen auszuführen. Betrachten Sie also die folgende Alternative:

var DIFF=0.5;                           // pixel increment
var MAX=Math.floor(5.0/DIFF);           // 5.0 is max pixel width
var STANDARD_LINE=Math.floor(1.5/DIFF); // 1.5 is pixel width

for(var i=0;i<=MAX;i++){
    button.text=(i*DIFF)+'';
    if(i==STANDARD_LINE){
      button.color='red';
    }
}

Die Schleife funktioniert in ganzen Zahlen. In diesem Fall ihandelt es sich um eine Ganzzahl, die ebenfalls STANDARD_LINEin eine Ganzzahl umgewandelt wird. Dies würde natürlich die Position Ihrer Standardlinie ändern, falls es zu einer Abrundung kommen MAXsollte. Sie sollten also weiterhin versuchen, eine Abrundung zu verhindern, um ein genaues Rendern zu erzielen. Sie haben jedoch immer noch den Vorteil, dass Sie die Parameter in Pixel und nicht in ganzen Zahlen ändern können, ohne sich um den Vergleich von Gleitkommawerten kümmern zu müssen.

Neil
quelle
3
Sie können auch Rundungen anstelle von Bodenbelägen in den Zuordnungen in Betracht ziehen, je nachdem, was Sie möchten. Wenn die Division ein ganzzahliges Ergebnis liefern soll, kann Floor Überraschungen bereiten, wenn Sie auf Zahlen stoßen, bei denen die Division zufällig leicht abweicht.
Ilkkachu
1
@ilkkachu Richtig. Ich dachte, wenn Sie 5.0 als maximale Pixelmenge festlegen, möchten Sie durch Abrunden lieber auf der unteren Seite von 5.0 als auf etwas mehr sein. 5,0 wäre effektiv ein Maximum. Je nachdem, was Sie tun müssen, ist eine Rundung möglicherweise vorzuziehen. In beiden Fällen ist es unerheblich, ob die Division trotzdem eine ganze Zahl erstellt.
Neil
4
Ich stimme überhaupt nicht zu. Der beste Weg, eine Schleife zu stoppen, ist die Bedingung, die die Geschäftslogik am natürlichsten zum Ausdruck bringt. Wenn die Geschäftslogik lautet, dass Sie 11 Schaltflächen benötigen, sollte die Schleife bei Iteration 11 anhalten. Wenn die Geschäftslogik lautet, dass die Schaltflächen 0,5 voneinander entfernt sind, bis die Leitung voll ist, sollte die Schleife anhalten, wenn die Leitung voll ist. Es gibt andere Überlegungen, die die Auswahl in Richtung des einen oder des anderen Mechanismus verschieben können. Wenn Sie diese Überlegungen jedoch nicht berücksichtigen, wählen Sie den Mechanismus aus, der den Geschäftsanforderungen am ehesten entspricht.
Setzen Sie Monica am
Ihre Erklärung wäre für Java / C ++ / Rubin / Python / völlig korrekt sein ... Aber Javascript nicht ganze Zahlen hat, so iund STANDARD_LINEnur wie ganze Zahlen aussehen. Es gibt keinen Zwang überhaupt, und DIFF, MAXund STANDARD_LINEsind alle nur Numbers. Numbers, die als Ganzzahlen verwendet werden, sollten unterhalb sicher sein 2**53, sie sind jedoch immer noch Gleitkommazahlen.
Eric Duminil
@EricDuminil Ja, aber das ist die Hälfte davon. Die andere Hälfte ist die Lesbarkeit. Ich nenne es als Hauptgrund dafür, und nicht zur Optimierung.
Neil
20

Ich stimme allen anderen Antworten zu, dass die Verwendung einer nicht ganzzahligen Schleifenvariablen im Allgemeinen ein schlechter Stil ist, selbst in Fällen wie diesem, in denen sie korrekt funktioniert. Aber es scheint mir, dass es einen anderen Grund gibt, warum es hier einen schlechten Stil gibt.

Ihr Code "weiß", dass die verfügbaren Linienbreiten genau ein Vielfaches von 0,5 von 0 bis 5,0 sind. Sollte es? Es scheint, dass dies eine Benutzeroberflächenentscheidung ist, die sich leicht ändern kann (z. B. möchten Sie, dass die Lücken zwischen den verfügbaren Breiten größer werden als die Breiten. 0,25, 0,5, 0,75, 1,0, 1,5, 2,0, 2,5, 3,0, 4,0, 5.0 oder so).

Ihr Code "weiß", dass alle verfügbaren Linienbreiten "schöne" Darstellungen sowohl als Gleitkommazahlen als auch als Dezimalzahlen haben. Das scheint sich auch zu ändern. (Möglicherweise möchten Sie irgendwann 0,1, 0,2, 0,3, ...)

Ihr Code "weiß", dass der Text auf den Schaltflächen einfach das ist, was Javascript aus diesen Gleitkommawerten macht. Das scheint sich auch zu ändern. (Beispiel: Vielleicht möchten Sie eines Tages Breiten wie 1/3, die Sie wahrscheinlich nicht als 0,33333333333333 oder was auch immer anzeigen möchten. Oder Sie möchten aus Konsistenzgründen "1,0" anstelle von "1" mit "1,5" anzeigen. .)

All dies fühlt sich für mich wie Manifestationen einer einzigen Schwäche an, die eine Art Vermischung von Schichten darstellt. Diese Gleitkommazahlen sind Teil der internen Logik der Software. Der auf den Schaltflächen angezeigte Text ist Teil der Benutzeroberfläche. Sie sollten getrennter sein als im Code hier. Begriffe wie "Welcher von diesen ist der Standard, der hervorgehoben werden sollte?" sind Fragen der Benutzeroberfläche, und sie sollten wahrscheinlich nicht an diese Gleitkommawerte gebunden werden. Und Ihre Schleife hier ist wirklich (oder sollte es zumindest sein) eine Schleife über Schaltflächen , nicht über Linienbreiten . So geschrieben, verschwindet die Versuchung, eine Schleifenvariable mit nicht ganzzahligen Werten zu verwenden: Sie würden nur aufeinanderfolgende ganze Zahlen oder eine for ... in / for ... of-Schleife verwenden.

Ich bin der Meinung, dass die meisten Fälle, in denen man versucht sein könnte, eine Schleife über nicht ganzzahlige Zahlen zu erstellen, wie folgt aussehen: Es gibt andere Gründe, die sich nicht auf numerische Probleme beziehen, weshalb der Code anders organisiert werden sollte. (Nicht in allen Fällen; ich kann mir vorstellen, dass einige mathematische Algorithmen am besten in Form einer Schleife über nicht ganzzahlige Werte ausgedrückt werden.)

Gareth McCaughan
quelle
8

Ein Code-Geruch ist die Verwendung von Floats in einer solchen Schleife.

Looping kann auf viele Arten durchgeführt werden, aber in 99,9% der Fälle sollten Sie sich an ein Inkrement von 1 halten, da es sonst definitiv zu Verwirrung kommt, nicht nur bei jungen Entwicklern.

Pieter B
quelle
Ich bin anderer Meinung, ich denke, dass ganzzahlige Vielfache von 1 in einer for-Schleife nicht verwirrend sind. Ich würde das nicht als Codegeruch betrachten. Nur Brüche.
CodeMonkey
3

Ja, das möchten Sie vermeiden.

Gleitkommazahlen sind eine der größten Fallen für den ahnungslosen Programmierer (was meiner Erfahrung nach fast jeden betrifft). Von der Abhängigkeit von Fließkomma-Gleichheitstests bis zur Darstellung von Geld als Fließkomma ist alles ein großer Sumpf. Einer der größten Straftäter ist das Aufsummieren eines Schwimmers auf den anderen. Es gibt ganze Bände wissenschaftlicher Literatur über solche Dinge.

Verwenden Sie Gleitkommazahlen genau an den Stellen, an denen sie angebracht sind, zum Beispiel, wenn Sie mathematische Berechnungen dort durchführen, wo Sie sie benötigen (wie Trigonometrie, Zeichnen von Funktionsgraphen usw.), und gehen Sie bei seriellen Operationen äußerst vorsichtig vor. Gleichstellung ist richtig. Das Wissen darüber, welche bestimmte Menge von Zahlen nach IEEE-Standards genau ist, ist sehr geheimnisvoll und ich würde mich niemals darauf verlassen.

In Ihrem Fall, es wird durch Murphy Law, kommt den Punkt , wo Management will Sie nicht 0,0 haben, 0,5, 1,0 ... aber 0,0, 0,4, 0,8 ... oder was auch immer; Sie werden sofort verdrängt und Ihr Junior-Programmierer (oder Sie selbst) wird lange und hart debuggen, bis Sie das Problem finden.

In Ihrem speziellen Code hätte ich in der Tat eine Integer-Loop-Variable. Es stellt die ith-Taste dar, nicht die laufende Nummer.

Und ich würde wahrscheinlich der zusätzlichen Klarheit halber nicht schreiben, i/2aber i*0.5das macht reichlich klar, was los ist.

var BUTTONS=11;
var STANDARD_LINE=3;

for(var i=0; i<BUTTONS; i++) {
    button.text = (i*0.5)+'';
    if (i==STANDARD_LINE) {
      button.color='red';
    }
}

Hinweis: Wie in den Kommentaren erwähnt, hat JavaScript keinen eigenen Typ für Ganzzahlen. Ganzzahlen mit bis zu 15 Stellen sind jedoch garantiert genau / sicher (siehe https://www.ecma-international.org/ecma-262/6.0/#sec-number.max_safe_integer ), daher für Argumente wie dieses ("ist es") verwirrender / fehleranfälliger für die Arbeit mit Ganzzahlen oder Nicht-Ganzzahlen Im täglichen Gebrauch (Schleifen, Bildschirmkoordinaten, Array-Indizes usw.) gibt es keine Überraschungen, wenn Ganzzahlen Numberals JavaScript dargestellt werden.

AnoE
quelle
Ich würde den Namen BUTTONS in etwas anderes ändern - es gibt immerhin 11 Buttons und nicht 10. Vielleicht FIRST_BUTTON = 0, LAST_BUTTON = 10, STANDARD_LINE_BUTTON = 3. Ansonsten sollten Sie es so machen.
gnasher729
Es ist wahr, @EricDuminil, und ich habe ein wenig darüber in die Antwort aufgenommen. Vielen Dank!
AnoE
1

Ich denke nicht, dass einer Ihrer Vorschläge gut ist. Stattdessen würde ich eine Variable für die Anzahl der Schaltflächen basierend auf dem Maximalwert und dem Abstand einführen. Dann ist es einfach genug, die Indizes der Schaltfläche selbst zu durchlaufen.

function precisionRound(number, precision) {
  let factor = Math.pow(10, precision);
  return Math.round(number * factor) / factor;
}

var maxButtonValue = 5.0;
var buttonSpacing = 0.5;

let countEstimate = precisionRound(maxButtonValue / buttonSpacing, 5);
var buttonCount = Math.floor(countEstimate) + 1;

var highlightPosition = 3;
var highlightColor = 'red';

for (let i=0; i < buttonCount; i++) {
    let buttonValue = i / buttonSpacing;
    button.text = buttonValue.toString();
    if (i == highlightPosition) {
        button.color = highlightColor;
    }
}

Es könnte mehr Code sein, aber es ist auch lesbarer und robuster.

Jared Goguen
quelle
0

Sie können das Ganze vermeiden, indem Sie den angezeigten Wert berechnen, anstatt den Schleifenzähler als Wert zu verwenden:

var MAX=5.0;
var DIFF=0.5
var STANDARD_LINE=1.5;

for(var i=0; (i*DIFF) < MAX ; i=i+1){
    var val = i * DIFF

    button.text=val+'';

    if(val==STANDARD_LINE){
      button.color='red';
    }
}
Arnab Datta
quelle
-1

Fließkomma-Arithmetik ist langsam und Ganzzahl-Arithmetik ist schnell. Wenn ich also Fließkomma verwende, würde ich es nicht unnötig verwenden, wenn Ganzzahlen verwendet werden können. Es ist nützlich, sich Fließkommazahlen und sogar Konstanten immer als ungefähr mit einem kleinen Fehler vorzustellen. Es ist beim Debuggen sehr nützlich, native Gleitkommazahlen durch Plus- / Minus-Gleitkommaobjekte zu ersetzen, bei denen Sie jede Zahl als Bereich anstelle eines Punkts behandeln. Auf diese Weise entdecken Sie nach jeder arithmetischen Operation zunehmende Ungenauigkeiten. "1.5" sollte also als "eine Zahl zwischen 1,45 und 1,55" und "1.50" als "eine Zahl zwischen 1,495 und 1,505" angesehen werden.

Jacquez
quelle
5
Der Leistungsunterschied zwischen Ganzzahlen und Gleitkommazahlen ist wichtig, wenn C-Code für einen kleinen Mikroprozessor geschrieben wird. Moderne x86-CPUs arbeiten jedoch so schnell mit Gleitkommazahlen, dass die Belastung durch die Verwendung einer dynamischen Sprache leicht zunichte gemacht werden kann. Insbesondere: Stellt Javascript nicht tatsächlich jede Zahl als Gleitkomma dar und verwendet bei Bedarf die NaN-Nutzdaten?
linksum den
1
"Fließkomma-Arithmetik ist langsam und Ganzzahl-Arithmetik ist schnell" ist eine historische Binsenweisheit, die Sie im weiteren Verlauf des Evangeliums nicht beibehalten sollten. Um zu dem, was @leftaroundabout sagte, hinzuzufügen, ist es nicht nur wahr, dass die Strafe fast irrelevant wäre. Möglicherweise finden Sie Gleitkommaoperationen schneller als die entsprechenden Ganzzahloperationen große Mengen von Schwimmern in einem Zyklus. Für diese Frage ist sie nicht relevant, aber die Grundannahme "Ganzzahl ist schneller als Float" ist schon eine ganze Weile nicht mehr zutreffend.
Jeroen Mostert
1
@JeroenMostert In SSE / AVX gibt es vektorisierte Operationen für Ganzzahlen und Gleitkommazahlen, und Sie können möglicherweise kleinere Ganzzahlen verwenden (da für Exponenten keine Bits verschwendet werden), sodass im Prinzip häufig noch mehr Leistung aus hochoptimiertem Ganzzahlcode herausgeholt wird als mit schwimmer. Aber auch dies ist für die meisten Anwendungen und definitiv nicht für diese Frage relevant.
Leftaroundabout
1
@ Linksaroundabout: Sicher. Es ging nicht darum, welcher in einer bestimmten Situation definitiv schneller ist , nur dass "Ich weiß, dass FP langsam und Integer schnell ist, also verwende ich, wenn möglich, Integer" keine gute Motivation ist, bevor Sie das angehen Frage, ob das, was Sie tun, optimiert werden muss.
Jeroen Mostert