Il confronto dell'uguaglianza dei numeri float inganna gli sviluppatori junior anche se nel mio caso non si verificano errori di arrotondamento?


31

Ad esempio, voglio mostrare un elenco di pulsanti da 0,0,5, ... 5, che salta per ogni 0,5. Uso un ciclo for per farlo e ho un colore diverso sul pulsante 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 questo caso non dovrebbero esserci errori di arrotondamento poiché ogni valore è esatto in IEEE 754, ma sto lottando se dovessi cambiarlo per evitare il confronto di uguaglianza in virgola mobile:

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';
    }
}

Da un lato, il codice originale è più semplice e avanti per me. Ma sto prendendo in considerazione una cosa: i == STANDARD_LINE induce in errore i compagni di squadra junior? Nasconde il fatto che i numeri in virgola mobile potrebbero avere errori di arrotondamento? Dopo aver letto i commenti di questo post:

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

sembra che ci siano molti sviluppatori che non sanno che alcuni numeri float sono esatti. Devo evitare i confronti dell'uguaglianza dei numeri float anche se è valido nel mio caso? O ci sto pensando troppo?


23
Il comportamento di questi due elenchi di codici non è equivalente. 3 / 2.0 è 1.5 ma isaranno sempre e solo numeri interi nella seconda lista. Prova a rimuovere il secondo /2.0.
candied_orange

27
Se hai assolutamente bisogno di confrontare due FP per l'uguaglianza (che non è richiesto come altri hanno sottolineato nelle loro belle risposte poiché puoi semplicemente usare un confronto del contatore di loop con numeri interi), ma se lo hai fatto, allora un commento dovrebbe essere sufficiente. Personalmente ho lavorato con IEEE FP per molto tempo e sarei ancora confuso se vedessi, diciamo, un confronto SPFP diretto senza alcun tipo di commento o altro. È solo un codice molto delicato - vale la pena commentare almeno ogni volta che IMHO.

14
Indipendentemente da quale scegli, questo è uno di quei casi in cui un commento che spiega come e perché è assolutamente essenziale. Uno sviluppatore successivo non può nemmeno considerare le sottigliezze senza un commento per portarle alla loro attenzione. Inoltre, sono fortemente distratto dal fatto che buttonnon cambia da nessuna parte nel tuo ciclo. Come si accede all'elenco dei pulsanti? Tramite l'indice in array o qualche altro meccanismo? Se si tratta dell'accesso all'indice in un array, questo è un altro argomento a favore del passaggio a numeri interi.
jpmc26

9
Scrivi quel codice. Fino a quando qualcuno non pensa che 0.6 sarebbe una dimensione del passo migliore e cambia semplicemente quella costante.
martedì

11
"... indurre in errore gli sviluppatori junior" Inganni anche gli sviluppatori senior. Nonostante la quantità di pensiero che hai messo in questo, presumeranno che tu non sapessi cosa stavi facendo e probabilmente lo cambieranno comunque nella versione intera.
GrandOpener,

Risposte:


116

Eviterei sempre le successive operazioni in virgola mobile a meno che il modello che sto calcolando non le richieda. L'aritmetica in virgola mobile non è intuitiva per la maggior parte e costituisce una fonte importante di errori. E raccontare i casi in cui provoca errori da quelli in cui non lo è è una distinzione ancora più sottile!

Pertanto, l'utilizzo di float come contatori di loop è un difetto in attesa di verificarsi e richiederebbe almeno un grosso commento di fondo che spieghi perché qui va bene usare 0,5 e che questo dipende dal valore numerico specifico. A quel punto, riscrivere il codice per evitare i contatori float sarà probabilmente l'opzione più leggibile. E la leggibilità è accanto alla correttezza nella gerarchia dei requisiti professionali.


48
Mi piace "un difetto in attesa di accadere". Certo, potrebbe funzionare ora , ma una leggera brezza da qualcuno che cammina vicino lo romperà.
AakashM,

10
Ad esempio, supponiamo che i requisiti cambino in modo che invece di 11 pulsanti equidistanti da 0 a 5 con la "linea standard" sul 4 ° pulsante, si disponga di 16 pulsanti equidistanti da 0 a 5 con la "linea standard" al 6 ° pulsante. Quindi chiunque abbia ereditato questo codice da te cambia da 0,5 a 1,0 / 3,0 e da 1,5 a 5,0 / 3,0. Cosa succede allora?
David K,

8
Sì, non mi sento a mio agio con l'idea che cambiando quello che sembra essere un numero arbitrario (come "normale" come potrebbe essere un numero) in un altro numero arbitrario (che sembra ugualmente "normale") in realtà introduce un difetto.
Alexander - Ripristina Monica il

7
@Alexander: giusto, avresti bisogno di un commento che dicesse DIFF must be an exactly-representable double that evenly divides STANDARD_LINE. Se non vuoi scrivere quel commento (e fare affidamento su tutti i futuri sviluppatori per sapere abbastanza sul punto mobile binario64 IEEE754 per capirlo), allora non scrivere il codice in questo modo. cioè non scrivere il codice in questo modo. Soprattutto perché probabilmente non è nemmeno più efficiente: l'aggiunta FP ha una latenza più elevata dell'aggiunta intera ed è una dipendenza trasportata da loop. Inoltre, i compilatori (anche i compilatori JIT?) Probabilmente fanno meglio a fare loop con contatori interi.
Peter Cordes,

39

Come regola generale, i loop dovrebbero essere scritti in modo tale da pensare di fare qualcosa n volte. Se stai usando indici a virgola mobile, non è più una questione di fare qualcosa n volte ma piuttosto di correre fino a quando una condizione è soddisfatta. Se questa condizione sembra essere molto simile a quella i<nche si aspettano così tanti programmatori, allora il codice sembra fare una cosa quando ne sta effettivamente facendo un'altra che può essere facilmente interpretata male dai programmatori che saltano il codice.

È in qualche modo soggettivo, ma secondo la mia modesta opinione, se riesci a riscrivere un ciclo per usare un indice intero per fare il ciclo per un numero fisso di volte, dovresti farlo. Quindi considera la seguente alternativa:

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';
    }
}

Il ciclo funziona in termini di numeri interi. In questo caso iè un numero intero ed STANDARD_LINEè anche forzato a un numero intero. Questo ovviamente cambierebbe la posizione della linea standard se ci fosse un roundoff e allo stesso MAXmodo, quindi dovresti comunque cercare di evitare il roundoff per un rendering accurato. Tuttavia, hai ancora il vantaggio di modificare i parametri in termini di pixel e non di numeri interi senza doverti preoccupare del confronto dei punti fluttuanti.


3
Potresti anche considerare di arrotondare invece di pavimentare nelle assegnazioni, a seconda di ciò che desideri. Se si suppone che la divisione dia un risultato intero, il piano potrebbe dare delle sorprese se ci si imbatte in numeri in cui la divisione risulta leggermente off.
ilkkachu,

1
@ilkkachu True. Pensavo che se si imposta 5.0 come quantità massima di pixel, quindi attraverso l'arrotondamento, si preferisce preferire essere sul lato inferiore di quel 5.0 piuttosto che leggermente più. 5.0 sarebbe effettivamente un massimo. Sebbene l'arrotondamento possa essere preferibile in base a ciò che è necessario fare. In entrambi i casi, fa poca differenza se la divisione crea comunque un numero intero.
Neil

4
Sono fortemente in disaccordo. Il modo migliore per interrompere un ciclo è la condizione che esprime in modo più naturale la logica aziendale. Se la logica aziendale è che sono necessari 11 pulsanti, il ciclo dovrebbe arrestarsi all'iterazione 11. Se la logica aziendale è che i pulsanti sono separati da 0,5 fino a quando la linea è piena, il ciclo dovrebbe arrestarsi quando la linea è piena. Esistono altre considerazioni che possono spingere la scelta verso un meccanismo o l'altro, ma in assenza di tali considerazioni, scegliere il meccanismo che più si avvicina alle esigenze aziendali.
Ripristina Monica il

La tua spiegazione sarebbe completamente corretta per Java / C ++ / Ruby / Python / ... Ma Javascript non ha numeri interi, quindi ie STANDARD_LINEsembrano solo numeri interi. Non c'è alcuna coercizione, e DIFF, MAXe STANDARD_LINEsono tutti solo Numbers. Numbers usati come numeri interi dovrebbero essere sicuri sotto 2**53, ma sono comunque numeri in virgola mobile.
Eric Duminil,

@EricDuminil Sì, ma questa è la metà. L'altra metà è leggibilità. Lo cito come il motivo principale per farlo in questo modo, non per l'ottimizzazione.
Neil,

20

Concordo con tutte le altre risposte sul fatto che l'utilizzo di una variabile di ciclo non intero è generalmente un cattivo stile anche in casi come questo in cui funzionerà correttamente. Ma mi sembra che ci sia un altro motivo per cui il cattivo stile qui.

Il tuo codice "sa" che le larghezze di linea disponibili sono precisamente i multipli di 0,5 da 0 a 5,0. Dovrebbe? Sembra che sia una decisione dell'interfaccia utente che potrebbe facilmente cambiare (ad esempio, forse si desidera che gli spazi tra le larghezze disponibili aumentino come fanno le larghezze. 0,25, 0,5, 0,75, 1,0, 1,5, 2,0, 2,5, 3,0, 4,0, 5.0 o qualcosa del genere).

Il tuo codice "sa" che le larghezze di linea disponibili hanno tutte "belle" rappresentazioni sia come numeri a virgola mobile che come decimali. Anche quello sembra qualcosa che potrebbe cambiare. (Potresti voler 0,1, 0,2, 0,3, ... ad un certo punto.)

Il tuo codice "sa" che il testo da mettere sui pulsanti è semplicemente ciò in cui Javascript trasforma quei valori in virgola mobile. Anche quello sembra qualcosa che potrebbe cambiare. (Ad esempio, forse un giorno vorrai larghezze come 1/3, che probabilmente non vorresti visualizzare come 0.33333333333333 o altro. O forse vuoi vedere "1.0" invece di "1" per coerenza con "1.5" .)

Mi sembrano tutte manifestazioni di una singola debolezza, che è una sorta di mescolanza di strati. Quei numeri in virgola mobile fanno parte della logica interna del software. Il testo mostrato sui pulsanti fa parte dell'interfaccia utente. Dovrebbero essere più separati di quanto non siano nel codice qui. Nozioni come "quale di queste è l'impostazione predefinita che deve essere evidenziata?" sono questioni relative all'interfaccia utente e probabilmente non dovrebbero essere legate a quei valori in virgola mobile. E il tuo loop qui è davvero (o almeno dovrebbe essere) un loop su pulsanti , non su larghezze di linea . Scritta in questo modo, la tentazione di usare una variabile di ciclo che accetta valori non interi scompare: useresti solo numeri interi successivi o un ciclo for ... in / for ... of.

La mia sensazione è che la maggior parte dei casi in cui si potrebbe essere tentati di passare in rassegna numeri non interi sono così: ci sono altre ragioni, completamente estranee alle questioni numeriche, per cui il codice dovrebbe essere organizzato in modo diverso. (Non tutti i casi; posso immaginare che alcuni algoritmi matematici possano essere espressi in modo più preciso in termini di un ciclo su valori non interi.)


8

Un odore di codice sta usando float in loop in quel modo.

Il looping può essere fatto in molti modi, ma nel 99,9% dei casi dovresti attenersi a un incremento di 1 o ci sarà sicuramente confusione, non solo dagli sviluppatori junior.


Non sono d'accordo, penso che i multipli interi di 1 non confondano in un ciclo for. Non considererei un odore di codice. Solo frazioni.
CodeMonkey,

3

Sì, vuoi evitare questo.

I numeri in virgola mobile sono una delle più grandi trappole per il programmatore ignaro (il che significa, nella mia esperienza, quasi tutti). Dal dipendere dai test di uguaglianza in virgola mobile, alla rappresentazione del denaro come virgola mobile, è tutto un grande pantano. Sommare un galleggiante sull'altro è uno dei più grandi delinquenti. Ci sono interi volumi di letteratura scientifica su cose come questa.

Utilizzare i numeri in virgola mobile esattamente nei punti in cui sono appropriati, ad esempio quando si eseguono calcoli matematici effettivi in ​​cui sono necessari (come trigonometria, rappresentazione grafica di grafici ecc.) E fare molta attenzione quando si eseguono operazioni seriali. L'uguaglianza è giusta. La conoscenza di quale particolare serie di numeri sia esatta per gli standard IEEE è molto arcana e non dipenderei mai da essa.

Nel tuo caso, non ci sarà , con la legge Murphys, arrivato al punto in cui gestione vuole che non si dispone di 0.0, 0.5, 1.0 ... ma 0.0, 0.4, 0.8 ... o qualsiasi altra cosa; verrai immediatamente borked e il tuo programmatore junior (o te stesso) eseguirà il debug a lungo e duramente fino a quando non trovi il problema.

Nel tuo particolare codice, avrei effettivamente una variabile di ciclo intero. Rappresenta il ipulsante th, non il numero corrente.

E probabilmente, per maggiore chiarezza, probabilmente non scriverò, i/2ma i*0.5ciò chiarirà abbondantemente cosa sta succedendo.

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';
    }
}

Nota: come sottolineato nei commenti, JavaScript non ha effettivamente un tipo separato per gli interi. Ma i numeri interi fino a 15 cifre sono garantiti come precisi / sicuri (vedi https://www.ecma-international.org/ecma-262/6.0/#sec-number.max_safe_integer ), quindi per argomenti come questo ("è più confuso / soggetto a errori nel lavorare con numeri interi o non ") questo è appropriatamente vicino ad avere un tipo separato" nello spirito "; nell'uso quotidiano (loop, coordinate dello schermo, indici di array ecc.) non ci saranno sorprese con numeri interi rappresentati Numbercome JavaScript.


Modificherei il nome BUTTONS in qualcos'altro - dopo tutto ci sono 11 pulsanti e non 10. Forse FIRST_BUTTON = 0, LAST_BUTTON = 10, STANDARD_LINE_BUTTON = 3. A parte questo, sì, è così che dovresti farlo.
gnasher729,

È vero, @EricDuminil, e ho aggiunto un po 'di questo nella risposta. Grazie!
AnoE

1

Non credo che nessuno dei tuoi suggerimenti sia buono. Invece, vorrei introdurre una variabile per il numero di pulsanti in base al valore massimo e alla spaziatura. Quindi, è abbastanza semplice passare in rassegna gli indici del pulsante stesso.

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;
    }
}

Potrebbe essere più codice, ma è anche più leggibile e più robusto.


0

Puoi evitare il tutto calcolando il valore che stai mostrando piuttosto che usare il contatore di loop come valore:

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';
    }
}

-1

L'aritmetica in virgola mobile è lenta e l'aritmetica di numeri interi è veloce, quindi quando uso il virgola mobile, non lo userei inutilmente laddove è possibile utilizzare numeri interi. È utile pensare sempre ai numeri in virgola mobile, anche alle costanti, come approssimativi, con qualche piccolo errore. Durante il debug è molto utile sostituire i numeri in virgola mobile nativi con oggetti in virgola mobile più / meno in cui si considera ogni numero come un intervallo anziché un punto. In questo modo si scoprono inesattezze progressivamente crescenti dopo ogni operazione aritmetica. Quindi "1,5" dovrebbe essere considerato come "un numero compreso tra 1,45 e 1,55", e "1,50" dovrebbe essere considerato come "un numero compreso tra 1,495 e 1,505".


5
La differenza di prestazioni tra numeri interi e float è importante quando si scrive codice C per un piccolo microprocessore, ma le moderne CPU derivate da x86 fanno virgola mobile così velocemente che qualsiasi sovraccarico viene facilmente eclissato dall'overhead dell'uso di un linguaggio dinamico. In particolare, Javascript non rappresenta comunque ogni numero come virgola mobile, usando il payload NaN quando necessario?
sinistra il

1
"L'aritmetica in virgola mobile è lenta e l'aritmetica intera è veloce" è un vero truismo che non dovresti conservare mentre il Vangelo avanza. Per aggiungere a ciò che ha detto @leftaroundabout, non è solo vero che la penalità sarebbe quasi irrilevante, ma potresti trovare le operazioni in virgola mobile più veloci delle loro equivalenti operazioni su numeri interi, grazie alla magia dei compilatori autovectorizing e dei set di istruzioni che possono scricchiolare grandi quantità di galleggianti in un ciclo. Per questa domanda non è rilevante, ma l'assunto di base "intero è più veloce del float" non è vero da un po 'di tempo.
Jeroen Mostert,

1
@JeroenMostert SSE / AVX hanno operazioni vettoriali per numeri interi e float, e potresti essere in grado di utilizzare numeri interi più piccoli (perché non vengono sprecati bit sull'esponente), quindi in linea di principio si potrebbe ancora ottenere maggiori prestazioni da un codice intero altamente ottimizzato che con i galleggianti. Ma ancora una volta, questo non è rilevante per la maggior parte delle applicazioni e sicuramente non per questa domanda.
lasciato il

1
@leftaroundabout: Sicuro. Il mio punto non riguardava quale sia decisamente più veloce in una determinata situazione, solo che "So che FP è lento e intero è veloce, quindi userò interi se possibile" non è una buona motivazione anche prima di affrontare il domanda se la cosa che stai facendo necessita di ottimizzazione.
Jeroen Mostert,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.