114 votes

Résultat à virgule flottante différent avec l'optimisation activée - bug du compilateur?

Le code ci-dessous fonctionne sur Visual Studio 2008 avec et sans optimisation. Mais il ne fonctionne que sur g++ sans optimisation (O0).

#include <cstdlib>
#include <iostream>
#include <cmath>

double round(double v, double digit)
{
    double pow = std::pow(10.0, digit);
    double t = v * pow;
    //std::cout << "t:" << t << std::endl;
    double r = std::floor(t + 0.5);
    //std::cout << "r:" << r << std::endl;
    return r / pow;
}

int main(int argc, char *argv[])
{
    std::cout << round(4.45, 1) << std::endl;
    std::cout << round(4.55, 1) << std::endl;
}

Le résultat devrait être:

4.5
4.6

Mais g++ avec optimisation (O1 - O3) donnera comme résultat:

4.5
4.5

Si j'ajoute le mot clé volatile avant t, cela fonctionne, y aurait-il un bug lié à l'optimisation?

Testé sur g++ 4.1.2 et 4.4.4.

Voici le résultat sur ideone: http://ideone.com/Rz937

Et l'option que j'ai testée sur g++ est simple:

g++ -O2 round.cpp

Le résultat le plus intéressant, même si j'active l'option /fp:fast sur Visual Studio 2008, le résultat reste correct.

Question supplémentaire:

Je me demandais, devrais-je toujours activer l'option -ffloat-store?

Parce que la version de g++ que j'ai testée est fournie avec CentOS/Red Hat Linux 5 et CentOS/Redhat 6.

J'ai compilé beaucoup de mes programmes sur ces plateformes, et je crains que cela ne provoque des bugs inattendus dans mes programmes. Il semble un peu difficile d'enquêter sur tout mon code C++ et les bibliothèques utilisées pour savoir s'ils ont de tels problèmes. Des suggestions?

Est-ce que quelqu'un s'intéresse au fait que même avec /fp:fast activé, Visual Studio 2008 fonctionne toujours? Il semble que Visual Studio 2008 soit plus fiable sur ce problème que g++?

52 votes

Aux nouveaux utilisateurs de SO : VOICI comment poser une question. +1

1 votes

FWIW, je reçois la bonne sortie avec g++ 4.5.0 en utilisant MinGW.

0 votes

Je reçois 4.5 4.6 pour tous les cas. Quelle est votre version de g++? J'ai g++ (Debian 4.3.2-1.1) 4.3.2

94voto

Maxim Yegorushkin Points 29380

Les processeurs Intel x86 utilisent une précision étendue de 80 bits en interne, alors que double est normalement sur 64 bits. Différents niveaux d'optimisation affectent la fréquence à laquelle les valeurs en virgule flottante du processeur sont sauvegardées en mémoire et donc arrondies de 80 bits de précision à 64 bits de précision.

Utilisez l'option -ffloat-store de gcc pour obtenir les mêmes résultats en virgule flottante avec différents niveaux d'optimisation.

Alternativement, utilisez le type long double, qui est normalement sur 80 bits avec gcc pour éviter l'arrondi de 80 bits à 64 bits de précision.

man gcc dit tout :

   -ffloat-store
       Ne stockez pas les variables en virgule flottante dans les registres, et inhibez
       d'autres options qui pourraient changer si une valeur en virgule flottante est
       obtenue depuis un registre ou de la mémoire.

       Cette option évite une précision excessive indésirable sur des machines telles que
       le 68000 où les registres flottants (du 68881) conservent plus
       de précision qu'un "double" est censé avoir. De même pour
       l'architecture x86. Pour la plupart des programmes, la précision excessive est
       uniquement bénéfique, mais quelques programmes reposent sur la définition précise de
       la virgule flottante IEEE. Utilisez -ffloat-store pour de tels programmes, après
       les avoir modifiés pour stocker toutes les calculs intermédiaires
       pertinents dans des variables.

Dans les compilations x86_64, les compilateurs utilisent par défaut les registres SSE pour les float et double, de sorte qu'aucune précision étendue n'est utilisée et ce problème ne se produit pas.

gcc option de compilation -mfpmath contrôle cela.

20 votes

Je pense que c'est la réponse. La constante 4,55 est convertie en 4,54999999999999 qui est la représentation binaire la plus proche en 64 bits; multipliez par 10 et arrondissez de nouveau à 64 bits et vous obtenez 45,5. Si vous sautez l'étape d'arrondissement en la conservant dans un registre de 80 bits, vous obtenez 45,4999999999999.

0 votes

Merci, je ne connais même pas cette option. Mais je me demandais, est-ce que je devrais toujours activer l'option -ffloat-store? Parce que la version de g++ que j'ai testée est livrée avec CentOS/Redhat 5 et CentOS/Redhat 6. J'ai compilé plusieurs de mes programmes sous ces plateformes, je crains que cela ne provoque des bugs inattendus à l'intérieur de mes programmes.

0 votes

@Mark: Mais comment expliquer si vous commentez l'instruction de débogage : std :: cout << "t:" << t << std :: endl ; la sortie sera correcte ???

10voto

David Hammen Points 17912

La sortie devrait être: 4,5 4,6 C'est ce que serait la sortie si vous aviez une précision infinie, ou si vous travailliez avec un appareil utilisant une représentation en virgule flottante basée sur des décimales plutôt que sur des bits. Mais ce n'est pas le cas. La plupart des ordinateurs utilisent la norme IEEE en virgule flottante binaire.

Comme Maxim Yegorushkin l'a déjà noté dans sa réponse, une partie du problème est que votre ordinateur utilise une représentation en virgule flottante de 80 bits. C'est juste une partie du problème. La base du problème est que tout nombre de la forme n.nn5 n'a pas de représentation exacte en virgule flottante binaire. Ces cas particuliers sont toujours des nombres inexacts.

Si vous voulez vraiment que votre arrondi puisse arrondir de manière fiable ces cas particuliers, vous avez besoin d'un algorithme d'arrondi qui tient compte du fait que n.n5, n.nn5, ou n.nnn5, etc. (mais pas n.5) est toujours inexact. Trouvez le cas particulier qui détermine si une valeur d'entrée doit être arrondie vers le haut ou vers le bas et renvoyez la valeur arrondie vers le haut ou vers le bas en fonction d'une comparaison avec ce cas particulier. Et vous devez faire attention à ce qu'un compilateur optimisant ne place pas ce cas particulier trouvé dans un registre de précision étendue.

Voir Comment Excel arrondit-il avec succès les nombres flottants même s'ils sont imprécis? pour un tel algorithme.

Ou vous pouvez simplement accepter le fait que les cas particuliers seront parfois arrondis de manière erronée.

6voto

Puppy Points 90818

Différents compilateurs ont des paramètres d'optimisation différents. Certains de ces paramètres d'optimisation plus rapides ne respectent pas strictement les règles des nombres flottants selon IEEE 754. Visual Studio a un réglage spécifique, /fp:strict, /fp:précis, /fp:rapide, où /fp:rapide viole la norme sur ce qui peut être fait. Vous pourriez constater que cet indicateur contrôle l'optimisation dans de telles situations. Vous pourriez également trouver un paramètre similaire dans GCC qui modifie le comportement.

Si c'est le cas, alors la seule différence entre les compilateurs est que GCC rechercherait par défaut le comportement des nombres flottants le plus rapide sur les optimisations supérieures, tandis que Visual Studio ne change pas le comportement des nombres flottants avec des niveaux d'optimisation plus élevés. Ainsi, il se pourrait qu'il ne s'agisse pas vraiment d'un bug, mais du comportement voulu d'une option que vous ne saviez pas que vous activiez.

4 votes

Il existe un commutateur -ffast-math pour GCC, et il n'est pas activé par aucun des niveaux d'optimisation -O car : "cela peut entraîner une sortie incorrecte pour les programmes qui dépendent d'une implémentation exacte des règles/spécifications IEEE ou ISO pour les fonctions mathématiques".

0 votes

@Mat: J'ai essayé -ffast-math et quelques autres choses sur mon g++ 4.4.3 et je suis toujours incapable de reproduire le problème.

0 votes

Avec -ffast-math, j'obtiens 4,5 dans les deux cas pour les niveaux d'optimisation supérieurs à 0.

4voto

tmandry Points 1197

La réponse acceptée est correcte si vous compilez vers une cible x86 qui n'inclut pas SSE2. Tous les processeurs x86 modernes prennent en charge SSE2, donc si vous pouvez en tirer parti, vous devriez :

-mfpmath=sse -msse2 -ffp-contract=off

Expliquons cela plus en détail.

-mfpmath=sse -msse2. Cela effectue l'arrondi en utilisant les registres SSE2, ce qui est beaucoup plus rapide que de stocker chaque résultat intermédiaire en mémoire. Notez que c'est déjà la valeur par défaut sur GCC pour x86-64. Selon le wiki GCC:

Sur les processeurs x86 plus modernes qui supportent SSE2, spécifier les options du compilateur -mfpmath=sse -msse2 garantit que toutes les opérations float et double sont effectuées dans des registres SSE et correctement arrondies. Ces options n'affectent pas l'ABI et doivent donc être utilisées chaque fois que possible pour des résultats numériques prévisibles.

-ffp-contract=off. Contrôler l'arrondi ne suffit pas pour une correspondance exacte, cependant. Les instructions FMA (fused multiply-add) peuvent modifier le comportement de l'arrondi par rapport à leurs homologues non fusionnées, donc nous devons les désactiver. C'est la valeur par défaut sur Clang, pas sur GCC. Comme expliqué par cette réponse:

Un FMA n'a qu'un seul arrondi (il garde effectivement une précision infinie pour le résultat de multiplication temporaire interne), tandis qu'un ADD + MUL en a deux.

En désactivant FMA, nous obtenons des résultats qui correspondent exactement en mode débogage et en mode release, au prix de certaines performances (et de la précision). Nous pouvons toujours tirer parti d'autres avantages de performance de SSE et AVX.

4voto

Max Lybbert Points 11822

A ceux qui ne peuvent pas reproduire le bug: ne décommentez pas les déclarations de debug commentées, elles affectent le résultat.

Cela implique que le problème est lié aux déclarations de debug. Et il semble qu'il y ait une erreur d'arrondi causée par le chargement des valeurs dans les registres lors des déclarations de sortie, c'est pourquoi d'autres ont constaté que vous pouvez corriger cela avec -ffloat-store

Question supplémentaire:

Je me demandais, devrais-je toujours activer l'option -ffloat-store?

Pour être frivole, il doit y avoir une raison pour laquelle certains programmeurs n'activent pas -ffloat-store, sinon l'option n'existerait pas (de même, il doit y avoir une raison pour laquelle certains programmeurs activent -ffloat-store). Je ne recommanderais pas de l'activer toujours ou de ne jamais l'activer. L'activer empêche certaines optimisations, mais le désactiver permet le genre de comportement que vous obtenez.

Mais, en général, il y a quelques divergences entre les nombres flottants binaires (comme les ordinateurs utilisent) et les nombres flottants décimaux (avec lesquels les gens sont familiers), et cette divergence peut causer un comportement similaire à ce que vous obtenez (pour être clair, le comportement que vous obtenez n'est pas causé par cette divergence, mais un comportement similaire peut l'être). La chose est que, puisque vous avez déjà une certaine imprécision lorsqu'il s'agit de flottants, je ne peux pas dire que -ffloat-store améliore ou détériore la situation.

A la place, vous pouvez envisager de trouver d'autres solutions au problème que vous essayez de résoudre (malheureusement, Koenig ne renvoie pas au papier réel, et je ne trouve pas vraiment de lieu "canonique" évident pour cela, donc je vais devoir vous renvoyer à Google).


Si vous ne faites pas d'arrondi à des fins de sortie, je regarderais probablement std::modf() (dans cmath) et std::numeric_limits::epsilon() (dans limits). En repensant à la fonction originale round(), je crois qu'il serait plus propre de remplacer l'appel à std::floor(d + .5) par un appel à cette fonction :

// cela a toujours les mêmes problèmes que la fonction d'arrondi originale
int arrondi_sup(double d)
{
    // la valeur de retour sera coercée en int, et tronquée comme attendu
    // vous pouvez ensuite assigner l'int à un double, si désiré
    return d + 0.5;
}

Je pense que cela suggère l'amélioration suivante :

// cela ne fonctionnera pas pour d négatif ...
// cela peut toujours arrondir certains nombres vers le haut alors qu'ils devraient être arrondis vers le bas
int arrondi_sup(double d)
{
    double plancher;
    d = std::modf(d, &plancher);
    return plancher + (d + .5 + std::numeric_limits::epsilon());
}

Une petite note : std::numeric_limits::epsilon() est défini comme "le plus petit nombre ajouté à 1 qui crée un nombre différent de 1". Vous devez généralement utiliser un epsilon relatif (c'est-à-dire, mettre à l'échelle epsilon d'une manière ou d'une autre pour tenir compte du fait que vous travaillez avec des nombres autres que "1"). La somme de d, .5 et std::numeric_limits::epsilon() devrait être proche de 1, donc le regroupement de cette addition signifie que std::numeric_limits::epsilon() sera à peu près de la bonne taille pour ce que nous faisons. Si tout se passe, std::numeric_limits::epsilon() sera trop grand (quand la somme des trois est inférieure à un) et peut nous amener à arrondir certains chiffres vers le haut alors que nous ne devrions pas le faire.


Aujourd'hui, vous devriez envisager std::nearbyint().

0 votes

Un "epsilon relatif" est appelé 1 ulp (1 unité dans la dernière place). x - nextafter(x, INFINITY) est lié à 1 ulp pour x (mais ne l'utilisez pas; je suis sûr qu'il y a des cas particuliers et je viens de l'inventer). L'exemple de cppreference pour epsilon() a un exemple de mise à l'échelle pour obtenir une erreur relative basée sur les ULP.

2 votes

Au fait, la réponse de 2016 à -ffloat-store est : ne pas utiliser x87 en premier lieu. Utilisez les mathématiques SSE2 (binaires 64 bits, ou -mfpmath=sse -msse2 pour les anciens binaires 32 bits démodés), car SSE/SSE2 dispose de temporaires sans précision supplémentaire. Les variables double et float dans les registres XMM sont vraiment au format IEEE 64 bits ou 32 bits. (Contrairement à x87, où les registres sont toujours de 80 bits, et le stockage en mémoire est arrondi à 32 ou 64 bits.)

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X