38 votes

le résultat du sinus dépend du compilateur C++ utilisé

J'utilise les deux compilateurs C++ suivants :

  • cl.exe : Compilateur optimisant Microsoft (R) C/C++ Version 19.00.24210 pour x86
  • g++ : g++ (Ubuntu 5.2.1-22ubuntu2) 5.2.1 20151010

En utilisant la fonction sinus intégrée, j'obtiens des résultats différents. Ce n'est pas critique, mais parfois les résultats sont trop significatifs pour mon usage. Voici un exemple avec une valeur "codée en dur" :

printf("%f\n", sin(5451939907183506432.0));

Résultat avec cl.exe :

0.528463

Résultat avec g++ :

0.522491

Je sais que le résultat de g++ est plus précis et que je pourrais utiliser une bibliothèque supplémentaire pour obtenir ce même résultat, mais ce n'est pas mon propos ici. Je voudrais vraiment comprendre ce qui se passe ici : pourquoi cl.exe est-il si mauvais ?

C'est drôle, si j'applique un modulo de (2 * pi) sur le paramètre, alors j'obtiens le même résultat que g++...

[EDIT] Juste parce que mon exemple semble fou pour certains d'entre vous : il s'agit d'une partie d'un générateur de nombres pseudo-aléatoires. Il n'est pas important de savoir si le résultat du sinus est exact ou non : nous avons juste besoin qu'il donne un certain résultat.

14 votes

Un coup d'œil au jeu d'instructions x86 permet de localiser les instructions mathématiques FSIN et FCOS, implémentées dans le matériel, de sorte que l'on peut s'attendre à ce que les résultats soient indépendants du compilateur. Peut-être que l'on sait que FSIN/FCOS est inexact avec de grandes valeurs, et que gcc fait un effort supplémentaire, et applique manuellement le modulo avant d'exécuter FSIN/FCOS.

0 votes

Par curiosité : pourquoi avez-vous besoin d'un argument aussi important ?

0 votes

@Henrik Ce n'est qu'un petit morceau d'algorithme : l'instruction précédente peut générer ce genre de valeurs énormes.

36voto

DAle Points 7149

Vous avez un littéral de 19 chiffres, mais le double a généralement une précision de 15 à 17 chiffres. Par conséquent, vous pouvez obtenir une petite erreur relative (lors de la conversion en double), mais une erreur absolue assez importante (dans le contexte du calcul du sinus).

En fait, les différentes implémentations de la bibliothèque standard présentent des différences dans le traitement de ces grands nombres. Par exemple, dans mon environnement, si on exécute

std::cout << std::fixed << 5451939907183506432.0;

Le résultat de g++ serait 5451939907183506432.000000
Le résultat de cl serait 5451939907183506400.000000

La différence vient du fait que les versions de cl antérieures à 19 ont un algorithme de formatage qui n'utilise qu'un nombre limité de chiffres et remplit les décimales restantes avec zéro.

En outre, regardons ce code :

double a[1000];
for (int i = 0; i < 1000; ++i) {
    a[i] = sin(5451939907183506432.0);
}
double d = sin(5451939907183506432.0);
cout << a[500] << endl;
cout << d << endl; 

Lorsqu'il est exécuté avec mon compilateur x86 VC++, le résultat est le suivant :

0.522491
0.528463

Il semble qu'en remplissant le tableau sin est compilé à l'appel de __vdecl_sin2 et lorsqu'il y a une seule opération, elle est compilée à l'appel de __libm_sse2_sin_precise (avec /fp:precise ).

A mon avis, votre nombre est trop important pour sin calcul pour s'attendre au même comportement de la part de différents compilateurs et pour s'attendre à un comportement correct en général.

4 votes

J'aurais pensé que les compilateurs produiraient exactement le même schéma binaire en interprétant ce littéral, ce qui n'expliquerait pas la différence de résultats.

2 votes

@SirGuy Probablement que ceci n'est pas spécifié par la norme, et fonctionne sous réserve des paramètres d'arrondi actuels et des drapeaux du compilateur.

0 votes

@SirGuy, Oui, c'est vrai. Ils ont la même valeur binaire.

16voto

SloopJon Points 268

Je pense que le commentaire de Sam est le plus proche de la réalité. Alors que vous utilisez une version récente de GCC/glibc, qui implémente sin() en logiciel (calculé au moment de la compilation pour le littéral en question), cl.exe pour x86 utilise probablement l'instruction fsin. Cette dernière peut être très imprécise, comme décrit dans l'article du blog Random ASCII, " Intel sous-estime les limites d'erreur de 1,3 quintillion ".

Une partie du problème avec votre exemple en particulier est qu'Intel utilise une approximation imprécise de pi lors de la réduction de la portée :

Lors de la réduction de gamme à partir d'une double précision (mantisse de 53 bits) pi les résultats auront environ 13 bits de précision (66 moins 53), pour une erreur allant jusqu'à 2^40 ULP (53 moins 13).

3 votes

Je pense également qu'il est très pertinent que dans gcc le compilateur fasse ce calcul et qu'avec cl, il soit fait par le code au moment de l'exécution. Je sais que gcc a fait beaucoup d'efforts pour s'assurer que le compilateur obtient exactement les mêmes réponses que le code qu'il génère. Mais cela les a obligés à réfléchir soigneusement à des questions comme celle-ci, et je soupçonne que cl n'a pas fait la même chose.

1 votes

L'exigence de précision qui sous-tend cet article de blog est-elle même "juste" ? À titre de comparaison, supposons que nous travaillons avec des points flottants ayant trois chiffres significatifs. Dans ce cas, pi est représenté par "3.14E0", mais il en va de même pour tout nombre compris entre 3.135 et 3.145, ce qui signifie que tout La sortie entre "-3.40E-3" et "+6.59E-3" (y compris "0") serait la valeur représentable la plus proche de sin(x) pour un certain x dont la valeur représentable la plus proche est "3.14". Exiger que la sortie de sin(3.14) soit "1.59E-3" me semble peu pratique...

0 votes

Vous pouvez le faire de manière plus précise et avec un code plus propre. La valeur de pi est connue avec des millions de décimales. Une méthode propre consiste à utiliser deux variables doubles, pi1 et pi2. pi1 stocke l'approximation en double précision la plus proche de pi, et pi2 stocke la meilleure approximation de l'"erreur" pi - pi1. Vous pouvez ensuite réduire la plage de l'argument x à sin(x) ou cos(x) avec pas de perte de précision, en mettant à l'échelle pi1 et pi2 par des puissances de 2, et en se rappelant que, conceptuellement, x peut être étendu avec un nombre "infini" de zéros binaires sans changer sa valeur.

9voto

GuyGreer Points 4240

Selon Référence cpp :

Le résultat peut avoir peu ou pas d'importance si la magnitude de arg est grande. (jusqu'à C++11)

Il est possible que cela soit la cause du problème, auquel cas vous voudrez effectuer manuellement le modulo de manière à ce que arg n'est pas grande.

1 votes

Je ne pense pas que faire manuellement le modulo va aider.

2 votes

Joshua, pourquoi pas ? Si un compilateur fait le modulo et l'autre non, cela pourrait expliquer la différence dans les résultats.

0 votes

Parce que le processeur x86 fait la mauvaise chose pour la fonction double sinus longue même en dessous du module. Ce que vous testez vraiment, c'est quelle libc corrige le mieux cette erreur.

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