La comparaison de l'égalité des nombres flottants induit-elle les développeurs juniors en erreur même si aucune erreur d'arrondi ne se produit dans mon cas?

31

Par exemple, je veux afficher une liste de boutons de 0,0,5, ... 5, qui saute pour chaque 0,5. J'utilise une boucle for pour cela, et j'ai une couleur différente au bouton 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';
    }
}

Dans ce cas, il ne devrait pas y avoir d'erreurs d'arrondi car chaque valeur est exacte dans IEEE 754. Mais je me bats si je dois la changer pour éviter la comparaison d'égalité en virgule flottante:

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

D'une part, le code d'origine est plus simple et me parvient. Mais il y a une chose que j'envisage: est-ce que i == STANDARD_LINE trompe ses coéquipiers juniors? Couvre-t-il le fait que les nombres à virgule flottante peuvent avoir des erreurs d'arrondi? Après avoir lu les commentaires de ce post:

/programming/33646148/is-hardcode-float-precise-if-it-can-be-represented-by-binary-format-in-ieee-754

il semble que de nombreux développeurs ne savent pas que certains nombres flottants sont exacts. Dois-je éviter les comparaisons d'égalité de nombres flottants même si elles sont valables dans mon cas? Ou est-ce que j'y réfléchis trop?

ocomfd
la source
23
Le comportement de ces deux listes de codes n'est pas équivalent. 3 / 2.0 est 1,5 mais ine sera que des nombres entiers dans la deuxième liste. Essayez de supprimer le second /2.0.
candied_orange
27
Si vous avez absolument besoin de comparer deux FP pour l'égalité (ce qui n'est pas requis comme d'autres l'ont souligné dans leurs bonnes réponses car vous pouvez simplement utiliser une comparaison de compteur de boucle avec des nombres entiers), mais si vous l'avez fait, un commentaire devrait suffire. Personnellement, je travaille avec IEEE FP depuis longtemps et je serais toujours confus si je voyais, disons, une comparaison directe du SPFP sans aucune sorte de commentaire ou quoi que ce soit. C'est juste un code très délicat - mérite un commentaire au moins à chaque fois à mon humble avis.
14
Quel que soit votre choix, c'est l'un de ces cas où un commentaire expliquant le comment et le pourquoi est absolument essentiel. Un développeur ultérieur peut même ne pas considérer les subtilités sans un commentaire pour les porter à leur attention. De plus, je suis fortement distrait par le fait que buttoncela ne change nulle part dans votre boucle. Comment accéder à la liste des boutons? Via un index dans un tableau ou un autre mécanisme? Si c'est par accès à un index dans un tableau, c'est un autre argument en faveur du passage aux entiers.
jpmc26
9
Écrivez ce code. Jusqu'à ce que quelqu'un pense que 0,6 serait une meilleure taille de pas et change simplement cette constante.
tofro
11
"... induire en erreur les développeurs juniors" Vous tromperez également les développeurs seniors. Malgré la quantité de réflexion que vous y avez mise, ils supposeront que vous ne saviez pas ce que vous faisiez, et la changeront probablement en version entière de toute façon.
GrandOpener

Réponses:

116

J'éviterais toujours les opérations successives en virgule flottante à moins que le modèle que je calcule ne les nécessite. L'arithmétique en virgule flottante n'est pas intuitive pour la plupart et constitue une source majeure d'erreurs. Et distinguer les cas dans lesquels il provoque des erreurs de ceux où il ne l'est pas est une distinction encore plus subtile!

Par conséquent, l'utilisation de flottants comme compteurs de boucles est un défaut qui attend de se produire et nécessiterait au moins un gros commentaire de fond expliquant pourquoi il est correct d'utiliser 0,5 ici, et que cela dépend de la valeur numérique spécifique. À ce stade, la réécriture du code pour éviter les compteurs flottants sera probablement l'option la plus lisible. Et la lisibilité est à côté de l'exactitude dans la hiérarchie des exigences professionnelles.

Kilian Foth
la source
48
J'aime "un défaut qui attend". Bien sûr, cela pourrait fonctionner maintenant , mais une légère brise de quelqu'un qui passera le cassera.
AakashM
10
Par exemple, supposons que les exigences changent de sorte qu'au lieu de 11 boutons également espacés de 0 à 5 avec la "ligne standard" sur le 4ème bouton, vous ayez 16 boutons également espacés de 0 à 5 avec la "ligne standard" sur le 6ème bouton. Ainsi, celui qui a hérité de ce code de vous passe de 0,5 à 1,0 / 3,0 et de 1,5 à 5,0 / 3,0. Que se passe-t-il alors?
David K
8
Ouais, je ne suis pas à l'aise avec l'idée que changer ce qui semble être un nombre arbitraire (aussi "normal" qu'un nombre pourrait l'être) en un autre nombre arbitraire (qui semble également "normal") introduit en fait un défaut.
Alexander - Reinstate Monica
7
@Alexander: à droite, vous auriez besoin d'un commentaire qui dit DIFF must be an exactly-representable double that evenly divides STANDARD_LINE. Si vous ne voulez pas écrire ce commentaire (et compter sur tous les futurs développeurs pour en savoir suffisamment sur la virgule flottante binaire64 IEEE754 pour le comprendre), alors n'écrivez pas le code de cette façon. c'est-à-dire n'écrivez pas le code de cette façon. Surtout parce qu'il n'est probablement pas encore plus efficace: l'addition FP a une latence plus élevée que l'addition entière, et c'est une dépendance portée par la boucle. De plus, les compilateurs (même les compilateurs JIT?) Font probablement mieux à faire des boucles avec des compteurs entiers.
Peter Cordes
39

En règle générale, les boucles doivent être écrites de manière à penser à faire quelque chose n fois. Si vous utilisez des indices à virgule flottante, il ne s'agit plus de faire quelque chose n fois mais de courir jusqu'à ce qu'une condition soit remplie. Si cette condition se trouve être très similaire à celle à i<nlaquelle tant de programmeurs s'attendent, alors le code semble faire une chose alors qu'il en fait une autre qui peut être facilement mal interprétée par les programmeurs en écrémant le code.

C'est quelque peu subjectif, mais à mon humble avis, si vous pouvez réécrire une boucle pour utiliser un index entier pour boucler un nombre fixe de fois, vous devriez le faire. Considérez donc l'alternative suivante:

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

La boucle fonctionne en termes de nombres entiers. Dans ce cas, iest un entier et STANDARD_LINEest également contraint à un entier. Cela changerait bien sûr la position de votre ligne standard s'il se produisait un arrondi et de même pour MAX, vous devriez donc vous efforcer d'empêcher l'arrondi pour un rendu précis. Cependant, vous avez toujours l'avantage de changer les paramètres en termes de pixels et non de nombres entiers sans avoir à vous soucier de la comparaison des virgules flottantes.

Neil
la source
3
Vous pouvez également envisager d'arrondir au lieu de revêtement de sol dans les affectations, selon ce que vous voulez. Si la division est censée donner un résultat entier, le plancher pourrait surprendre si vous tombez sur des nombres où la division est légèrement décalée.
ilkkachu
1
@ilkkachu True. Mes pensées étaient que si vous définissez 5,0 comme la quantité maximale de pixels, puis en arrondissant, vous aimeriez de préférence être sur le côté inférieur de ce 5,0 plutôt que légèrement plus. 5.0 serait effectivement un maximum. L'arrondi peut être préférable selon ce que vous devez faire. Dans les deux cas, cela fait peu de différence si la division crée de toute façon un nombre entier.
Neil
4
Je suis fortement en désaccord. La meilleure façon d'arrêter une boucle est la condition qui exprime le plus naturellement la logique métier. Si la logique métier est que vous avez besoin de 11 boutons, la boucle doit s'arrêter à l'itération 11. Si la logique métier est que les boutons sont séparés de 0,5 jusqu'à ce que la ligne soit pleine, la boucle doit s'arrêter lorsque la ligne est pleine. Il existe d'autres considérations qui peuvent pousser le choix vers un mécanisme ou l'autre, mais en l'absence de ces considérations, choisissez le mécanisme qui correspond le mieux aux besoins de l'entreprise.
Rétablir Monica le
Votre explication serait tout à fait correcte pour Java / C ++ / Ruby / Python / ... Mais Javascript n'a pas d'entiers, iet STANDARD_LINEne ressemble donc qu'à des entiers. Il n'y a aucune contrainte du tout, et DIFF, MAXet ce ne STANDARD_LINEsont que des Numberart. NumberLes s utilisés comme entiers doivent être sûrs ci 2**53- dessous , mais ils sont toujours des nombres à virgule flottante.
Eric Duminil
@EricDuminil Oui, mais c'est la moitié. L'autre moitié est la lisibilité. Je le mentionne comme la principale raison de le faire de cette façon, pas pour l'optimisation.
Neil
20

Je suis d'accord avec toutes les autres réponses selon lesquelles l'utilisation d'une variable de boucle non entière est généralement un mauvais style même dans des cas comme celui-ci où cela fonctionnera correctement. Mais il me semble qu'il y a une autre raison pour laquelle c'est un mauvais style ici.

Votre code "sait" que les largeurs de ligne disponibles sont précisément les multiples de 0,5 de 0 à 5,0. Devrait-il? Il semble que ce soit une décision d'interface utilisateur qui pourrait facilement changer (par exemple, vous voulez peut-être que les écarts entre les largeurs disponibles deviennent plus grands comme le font les largeurs. 0,25, 0,5, 0,75, 1,0, 1,5, 2,0, 2,5, 3,0, 4,0, 5,0 ou quelque chose).

Votre code "sait" que les largeurs de ligne disponibles ont toutes de "belles" représentations à la fois sous forme de nombres à virgule flottante et sous forme de décimales. Cela semble également quelque chose qui pourrait changer. (Vous voudrez peut-être 0,1, 0,2, 0,3, ... à un moment donné.)

Votre code "sait" que le texte à mettre sur les boutons est simplement ce en quoi Javascript transforme ces valeurs à virgule flottante. Cela semble également quelque chose qui pourrait changer. (Par exemple, peut-être qu'un jour vous voudrez des largeurs comme 1/3, que vous ne voudriez probablement pas afficher comme 0.33333333333333 ou autre. Ou peut-être que vous voulez voir "1.0" au lieu de "1" pour la cohérence avec "1.5" .)

Tout cela me semble être les manifestations d'une seule faiblesse, qui est une sorte de mélange de couches. Ces nombres à virgule flottante font partie de la logique interne du logiciel. Le texte affiché sur les boutons fait partie de l'interface utilisateur. Ils devraient être plus séparés que dans le code ici. Des notions telles que "laquelle est la valeur par défaut à mettre en évidence?" sont des questions d'interface utilisateur, et elles ne devraient probablement pas être liées à ces valeurs à virgule flottante. Et votre boucle ici est vraiment (ou du moins devrait être) une boucle sur des boutons , pas sur des largeurs de ligne . Écrit de cette façon, la tentation d'utiliser une variable de boucle prenant des valeurs non entières disparaît: vous utiliseriez simplement des entiers successifs ou une boucle for ... in / for ... of.

Mon sentiment est que la plupart des cas où l'on pourrait être tenté de parcourir des nombres non entiers sont comme ceci: il y a d'autres raisons, totalement indépendantes des problèmes numériques, pour lesquelles le code devrait être organisé différemment. (Pas tous les cas; je peux imaginer que certains algorithmes mathématiques pourraient être exprimés de manière plus nette en termes de boucle sur des valeurs non entières.)

Gareth McCaughan
la source
8

Une odeur de code utilise des flotteurs en boucle comme ça.

Le bouclage peut se faire de nombreuses façons, mais dans 99,9% des cas, vous devriez vous en tenir à un incrément de 1 ou il y aura certainement de la confusion, non seulement par les développeurs juniors.

Pieter B
la source
Je ne suis pas d'accord, je pense que les multiples entiers de 1 ne prêtent pas à confusion dans une boucle for. Je ne considérerais pas cela comme une odeur de code. Seulement des fractions.
CodeMonkey
3

Oui, vous voulez éviter cela.

Les nombres à virgule flottante sont l'un des plus grands pièges pour le programmeur sans méfiance (ce qui signifie, selon mon expérience, presque tout le monde). De dépendre des tests d'égalité en virgule flottante à représenter l'argent en virgule flottante, tout est un gros bourbier. Ajouter un flotteur sur l'autre est l'un des plus grands contrevenants. Il y a des volumes entiers de littérature scientifique sur des choses comme ça.

Utilisez des nombres à virgule flottante exactement aux endroits où ils sont appropriés, par exemple lorsque vous effectuez des calculs mathématiques réels là où vous en avez besoin (comme la trigonométrie, les graphiques de fonction de traçage, etc.) et soyez très prudent lorsque vous effectuez des opérations en série. L'égalité est au rendez-vous. La connaissance de quel ensemble particulier de nombres est exact selon les normes IEEE est très mystérieuse et je n'en dépendrais jamais.

Dans votre cas, il y aura , par la loi Murphys, le moment où la direction voudra que vous n'ayez pas 0,0, 0,5, 1,0 ... mais 0,0, 0,4, 0,8 ... ou autre; vous vous serez immédiatement borked, et votre programmeur junior (ou vous-même) déboguera longtemps et dur jusqu'à ce que vous trouviez le problème.

Dans votre code particulier, j'aurais en effet une variable de boucle entière. Il représente le ibouton e, pas le numéro courant.

Et je voudrais probablement, dans un souci de clarté supplémentaire, ne pas écrire i/2mais i*0.5ce qui rend très clair ce qui se passe.

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

Remarque: comme indiqué dans les commentaires, JavaScript n'a pas de type distinct pour les entiers. Mais les entiers jusqu'à 15 chiffres sont garantis pour être précis / sûr (voir https://www.ecma-international.org/ecma-262/6.0/#sec-number.max_safe_integer ), donc pour des arguments comme celui-ci ("est-ce plus source de confusion / d’erreurs susceptibles de fonctionner avec des entiers ou des non-entiers "), il est tout à fait approprié d’avoir un type distinct" dans l’esprit "; dans l'utilisation quotidienne (boucles, coordonnées d'écran, indices de tableau, etc.) il n'y aura pas de surprise avec des nombres entiers représentés Numbercomme JavaScript.

AnoE
la source
Je changerais le nom BUTTONS en autre chose - il y a 11 boutons après tout et pas 10. Peut-être FIRST_BUTTON = 0, LAST_BUTTON = 10, STANDARD_LINE_BUTTON = 3. En dehors de cela, oui, c'est comme ça que vous devriez le faire.
gnasher729
C'est vrai, @EricDuminil, et j'ai ajouté un peu à ce sujet dans la réponse. Merci!
AnoE
1

Je ne pense pas que vos suggestions soient bonnes. Au lieu de cela, j'introduirais une variable pour le nombre de boutons en fonction de la valeur maximale et de l'espacement. Ensuite, il est assez simple de parcourir les index du bouton eux-mêmes.

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

Il peut s'agir de plus de code, mais il est également plus lisible et plus robuste.

Jared Goguen
la source
0

Vous pouvez éviter tout cela en calculant la valeur que vous affichez plutôt qu'en utilisant le compteur de boucle comme valeur:

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
la source
-1

L'arithmétique à virgule flottante est lente et l'arithmétique à nombres entiers est rapide, donc lorsque j'utilise des virgules flottantes, je ne les utiliserais pas inutilement là où des entiers peuvent être utilisés. Il est utile de toujours considérer les nombres à virgule flottante, même les constantes, comme approximatifs, avec quelques petites erreurs. Il est très utile lors du débogage de remplacer les nombres à virgule flottante natifs par des objets à virgule flottante plus / moins où vous traitez chaque nombre comme une plage au lieu d'un point. De cette façon, vous découvrez des inexactitudes croissantes progressives après chaque opération arithmétique. Ainsi, "1.5" doit être considéré comme "un nombre compris entre 1.45 et 1.55" et "1.50" doit être considéré comme "un nombre compris entre 1.495 et 1.505".

Jacquez
la source
5
La différence de performances entre les nombres entiers et les flottants est importante lors de l'écriture de code C pour un petit microprocesseur, mais les processeurs dérivés de x86 modernes flottent si rapidement que toute pénalité est facilement éclipsée par la surcharge d'utilisation d'un langage dynamique. En particulier, Javascript ne représente-t-il pas réellement chaque nombre comme virgule flottante, en utilisant la charge utile NaN lorsque cela est nécessaire?
leftaroundabout
1
"L'arithmétique en virgule flottante est lente et l'arithmétique entière est rapide" est un truisme historique que vous ne devriez pas retenir comme évangile pour aller de l'avant. Pour ajouter à ce que @leftaroundabout a dit, il n'est pas seulement vrai que la pénalité serait presque hors de propos, vous pourriez bien trouver que les opérations en virgule flottante sont plus rapides que leurs opérations entières équivalentes, grâce à la magie de l'autovectorisation des compilateurs et des jeux d'instructions qui peuvent crunch de grandes quantités de flotteurs en un seul cycle. Pour cette question, ce n'est pas pertinent, mais l'hypothèse de base «l'entier est plus rapide que le flottant» n'est pas vraie depuis un bon moment.
Jeroen Mostert
1
@JeroenMostert SSE / AVX ont des opérations vectorisées pour les entiers et les flottants, et vous pouvez peut-être utiliser des entiers plus petits (car aucun bit n'est gaspillé sur l'exposant), donc en principe, on peut souvent encore obtenir plus de performances à partir d'un code entier hautement optimisé qu'avec des flotteurs. Mais encore une fois, cela n'est pas pertinent pour la plupart des applications et certainement pas pour cette question.
leftaroundabout
1
@leftaroundabout: Bien sûr. Mon point n'était pas de savoir lequel est définitivement le plus rapide dans une situation donnée, juste que "je sais que la FP est lente et que l'entier est rapide, donc j'utiliserai des entiers si possible" n'est pas une bonne motivation avant même de s'attaquer à la question de savoir si la chose que vous faites doit être optimisée.
Jeroen Mostert