web-dev-qa-db-fra.com

Résultat différent en virgule flottante avec optimisation activée - bogue du compilateur?

Le code ci-dessous fonctionne sur Visual Studio 2008 avec et sans optimisation. Mais cela 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;
}

La sortie doit être:

4.5
4.6

Mais g ++ avec optimisation (O1 - O3) affichera:

4.5
4.5

Si j'ajoute le mot clé volatile avant t, cela fonctionne, alors peut-il y avoir une sorte de bogue d'optimisation?

Testez sur g ++ 4.1.2 et 4.4.4.

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

Et l'option que je teste sur g ++ est simple:

g++ -O2 round.cpp

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

Autre question:

Je me demandais, devrais-je toujours allumer le -ffloat-store option?

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

J'ai compilé plusieurs de mes programmes sous ces plates-formes, et je crains que cela ne provoque des bugs inattendus dans mes programmes. Il semble un peu difficile d'étudier tout mon code C++ et les bibliothèques utilisées s'ils ont de tels problèmes. Toute suggestion?

Quelqu'un veut-il savoir pourquoi même /fp:fast activé, Visual Studio 2008 fonctionne toujours? Il semble que Visual Studio 2008 soit plus fiable à ce problème que g ++?

102
Bear

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

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

Vous pouvez également utiliser le long double type, qui est normalement large de 80 bits sur gcc pour éviter l'arrondi de la précision de 80 bits à 64 bits.

man gcc dit tout:

   -ffloat-store
       Do not store floating point variables in registers, and inhibit
       other options that might change whether a floating point value is
       taken from a register or memory.

       This option prevents undesirable excess precision on machines such
       as the 68000 where the floating registers (of the 68881) keep more
       precision than a "double" is supposed to have.  Similarly for the
       x86 architecture.  For most programs, the excess precision does
       only good, but a few programs rely on the precise definition of
       IEEE floating point.  Use -ffloat-store for such programs, after
       modifying them to store all pertinent intermediate computations
       into variables.
86
Maxim Egorushkin

La sortie devrait être: 4.5 4.6 C'est ce que la sortie serait si vous aviez une précision infinie, ou si vous travailliez avec un appareil qui utilisait une représentation en virgule flottante décimale plutôt que binaire. Mais tu ne l'es pas. La plupart des ordinateurs utilisent la norme binaire à virgule flottante IEEE.

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

Si vous voulez vraiment que votre arrondi puisse arrondir de manière fiable ces cas d'angle, 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. Recherchez le cas d'angle qui détermine si une valeur d'entrée arrondit vers le haut ou vers le bas et retournez la valeur arrondie vers le haut ou vers le bas en fonction d'une comparaison avec ce cas d'angle. Et vous devez vous assurer qu'un compilateur d'optimisation ne placera pas ce cas de coin trouvé dans un registre de précision étendu.

Voir Comment Excel réussit-il à arrondir les nombres flottants même s'ils sont imprécis? pour un tel algorithme.

Ou vous pouvez simplement vivre avec le fait que les boîtiers d'angle arrondiront parfois par erreur.

10
David Hammen

Différents compilateurs ont des paramètres d'optimisation différents. Certains de ces paramètres d'optimisation plus rapides ne maintiennent pas de règles strictes en virgule flottante selon IEEE 754 . Visual Studio a un paramètre spécifique, /fp:strict, /fp:precise, /fp:fast, où /fp:fast viole la norme sur ce qui peut être fait. Vous pourriez constater que cet indicateur est ce qui contrôle l'optimisation dans de tels paramètres. Vous pouvez également trouver un paramètre similaire dans GCC qui modifie le comportement.

Si tel est le cas, la seule chose différente entre les compilateurs est que GCC recherche par défaut le comportement en virgule flottante le plus rapide sur les optimisations supérieures, tandis que Visual Studio ne modifie pas le comportement en virgule flottante avec des niveaux d'optimisation plus élevés. Il ne s'agit donc pas nécessairement d'un bug réel, mais du comportement prévu d'une option que vous ne saviez pas que vous allumiez.

6
Puppy

Pour ceux qui ne peuvent pas reproduire le bogue: ne commentez pas les stmts de débogage commentés, ils affectent le résultat.

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

Autre question:

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

Pour être désinvolte, 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 le font activez -ffloat-store). Je ne recommanderais pas de toujours l'allumer ou de l'éteindre toujours. L'activer empêche certaines optimisations, mais la désactiver permet le type de comportement que vous obtenez.

Mais, en général, il y a n certain décalage entre les nombres à virgule flottante binaires (comme l'ordinateur utilise) et les nombres à virgule flottante décimaux (que les gens connaissent), et ce décalage peut provoquer un comportement similaire à ce que vous obtenez (pour être clair, le comportement que vous obtenez n'est pas pas causé par ce décalage, mais un comportement similaire peut être). Le fait est que, puisque vous avez déjà un certain flou lorsque vous traitez avec une virgule flottante, je ne peux pas dire que -ffloat-store Le rend meilleur ou pire.

Au lieu de cela, vous voudrez peut-être examiner autres solutions au problème que vous essayez de résoudre (malheureusement, Koenig ne pointe pas vers le papier réel, et je ne peux pas vraiment trouver une "canonique" évidente). "place pour cela, je vais donc devoir vous envoyer à Google ).


Si vous n'êtes pas arrondi à des fins de sortie, je regarderais probablement std::modf() (dans cmath) et std::numeric_limits<double>::epsilon() (dans limits). En pensant à la fonction round() d'origine, je pense qu'il serait plus propre de remplacer l'appel à std::floor(d + .5) par un appel à cette fonction:

// this still has the same problems as the original rounding function
int round_up(double d)
{
    // return value will be coerced to int, and truncated as expected
    // you can then assign the int to a double, if desired
    return d + 0.5;
}

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

// this won't work for negative d ...
// this may still round some numbers up when they should be rounded down
int round_up(double d)
{
    double floor;
    d = std::modf(d, &floor);
    return floor + (d + .5 + std::numeric_limits<double>::epsilon());
}

Une note simple: std::numeric_limits<T>::epsilon() est définie 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<double>::epsilon() devrait être proche de 1, donc le regroupement de cet ajout signifie que std::numeric_limits<double>::epsilon() sera à peu près de la bonne taille pour quoi faisaient. Si quelque chose, std::numeric_limits<double>::epsilon() sera trop grand (lorsque la somme des trois est inférieure à un) et peut nous amener à arrondir certains chiffres alors que nous ne devrions pas.


De nos jours, vous devriez considérer std::nearbyint() .

4
Max Lybbert

J'ai approfondi ce problème et je peux apporter plus de précisions. Tout d'abord, les représentations exactes de 4.45 et 4.55 selon gcc sur x84_64 sont les suivantes (avec libquadmath pour imprimer la dernière précision):

float 32:   4.44999980926513671875
double 64:  4.45000000000000017763568394002504646778106689453125
doublex 80: 4.449999999999999999826527652402319290558807551860809326171875
quad 128:   4.45000000000000000000000000000000015407439555097886824447823540679418548304813185723105561919510364532470703125

float 32:   4.55000019073486328125
double 64:  4.54999999999999982236431605997495353221893310546875
doublex 80: 4.550000000000000000173472347597680709441192448139190673828125
quad 128:   4.54999999999999999999999999999999984592560444902113175552176459320581451695186814276894438080489635467529296875

Comme Maxim dit ci-dessus, le problème est dû à la taille de 80 bits des registres FPU.

Mais pourquoi le problème ne se produit-il jamais sous Windows? sur IA-32, le FPU x87 a été configuré pour utiliser une précision interne pour la mantisse de 53 bits (équivalant à une taille totale de 64 bits: double). Pour Linux et Mac OS, la précision par défaut de 64 bits a été utilisée (équivalant à une taille totale de 80 bits: long double). Le problème devrait donc être possible, ou non, sur ces différentes plates-formes en changeant le mot de contrôle du FPU (en supposant que la séquence d'instructions déclencherait le bug). Le problème a été signalé à gcc comme bug 32 (lire au moins le commentaire 92!).

Pour montrer la précision de la mantisse sous Windows, vous pouvez la compiler en 32 bits avec VC++:

#include "stdafx.h"
#include <stdio.h>  
#include <float.h>  

int main(void)
{
    char t[] = { 64, 53, 24, -1 };
    unsigned int cw = _control87(0, 0);
    printf("mantissa is %d bits\n", t[(cw >> 16) & 3]);
}

et sur Linux/Cygwin:

#include <stdio.h>

int main(int argc, char **argv)
{
    char t[] = { 24, -1, 53, 64 };
    unsigned int cw = 0;
    __asm__ __volatile__ ("fnstcw %0" : "=m" (*&cw));
    printf("mantissa is %d bits\n", t[(cw >> 8) & 3]);
}

Notez qu'avec gcc, vous pouvez définir la précision FPU avec -mpc32/64/80, Bien qu'elle soit ignorée dans Cygwin. Mais gardez à l'esprit que cela modifiera la taille de la mantisse, mais pas celle de l'exposant, laissant la porte ouverte à d'autres types de comportements différents.

Sur l'architecture x86_64, SSE est utilisé comme dit par tmandry , donc le problème ne se produira que si vous forcez l'ancien FPU x87 pour FP à calculer avec -mfpmath=387 , ou à moins que vous ne compiliez en mode 32 bits avec -m32 (vous aurez besoin du paquetage multilib). Je pourrais reproduire le problème sur Linux avec différentes combinaisons de drapeaux et de versions de gcc:

g++-5 -m32 floating.cpp -O1
g++-8 -mfpmath=387 floating.cpp -O1

J'ai essayé quelques combinaisons sur Windows ou Cygwin avec VC++/gcc/tcc mais le bug ne s'est jamais présenté. Je suppose que la séquence d'instructions générée n'est pas la même.

Enfin, notez qu'un moyen exotique pour éviter ce problème avec 4.45 ou 4.55 serait d'utiliser _Decimal32/64/128, Mais le support est vraiment rare ... J'ai passé beaucoup de temps juste pour pouvoir faire un printf avec libdfp!

1
calandoa

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 profiter, vous devez:

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

Décomposons cela.

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

Sur les processeurs x86 plus récents qui prennent en charge SSE2, spécifiez les options du compilateur -mfpmath=sse -msse2 garantit que toutes les opérations flottantes et doubles sont effectuées dans les registres SSE et arrondis correctement. Ces options n'affectent pas l'ABI et doivent donc être utilisées dans la mesure du possible pour des résultats numériques prévisibles.

-ffp-contract=off. Cependant, contrôler l'arrondi ne suffit pas pour une correspondance exacte. Les instructions FMA (Fused Multiply-Add) peuvent changer le comportement d'arrondi par rapport à leurs homologues non fusionnés, nous devons donc le désactiver. C'est la valeur par défaut sur Clang, pas GCC. Comme expliqué par cette réponse :

Un FMA n'a qu'un seul arrondi (il conserve 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 au débogage et à la publication, au détriment de certaines performances (et de la précision). Nous pouvons toujours profiter d'autres avantages de performances de SSE et AVX.

1
tmandry

Personnellement, j'ai rencontré le même problème dans l'autre sens - de gcc à VS. Dans la plupart des cas, je pense qu'il vaut mieux éviter l'optimisation. La seule fois où cela en vaut la peine est lorsque vous avez affaire à des méthodes numériques impliquant de grands tableaux de données à virgule flottante. Même après le démontage, je suis souvent déçu par les choix des compilateurs. Très souvent, il est plus facile d'utiliser les éléments intrinsèques du compilateur ou d'écrire l'assembly vous-même.

0
cdcdcd