La version en ligne d'une fonction renvoie une valeur différente de celle de la version non en ligne

85

Comment deux versions de la même fonction, ne différant que l'une étant en ligne et l'autre non, peuvent-elles renvoyer des valeurs différentes? Voici un code que j'ai écrit aujourd'hui et je ne suis pas sûr de son fonctionnement.

#include <cmath>
#include <iostream>

bool is_cube(double r)
{
    return floor(cbrt(r)) == cbrt(r);
}

bool inline is_cube_inline(double r)
{
    return floor(cbrt(r)) == cbrt(r);
}

int main()
{
    std::cout << (floor(cbrt(27.0)) == cbrt(27.0)) << std::endl;
    std::cout << (is_cube(27.0)) << std::endl;
    std::cout << (is_cube_inline(27.0)) << std::endl;
}

Je m'attendrais à ce que toutes les sorties soient égales à 1, mais il génère en fait ceci (g ++ 8.3.1, pas de drapeaux):

1
0
1

au lieu de

1
1
1

Edit: clang ++ 7.0.0 génère ceci:

0
0
0

et g ++ -Ofast ceci:

1
1
1
zbrojny120
la source
3
Pouvez-vous s'il vous plaît fournir quel compilateur, quelles options de compilateur utilisez-vous et quelle machine? Fonctionne bien pour moi sur GCC 7.1 sous Windows.
Diodacus
31
N'est-ce pas ==toujours un peu imprévisible avec des valeurs en virgule flottante?
500 - Erreur de serveur interne
3
associé stackoverflow.com/questions/588004/…
idclev 463035818
2
Avez-vous défini l' -Ofastoption, qui permet de telles optimisations?
cmdLP
4
Le compilateur renvoie cbrt(27.0)la valeur de 0x0000000000000840tandis que la bibliothèque standard retourne 0x0100000000000840. Les doubles diffèrent par le 16e numéro après la virgule. Mon système: archlinux4.20 x64 gcc8.2.1 glibc2.28 Vérifié avec ceci . Je me demande si gcc ou glibc a raison.
KamilCuk

Réponses:

73

Explication

Certains compilateurs (notamment GCC) utilisent une précision plus élevée lors de l'évaluation des expressions au moment de la compilation. Si une expression dépend uniquement d'entrées et de littéraux constants, elle peut être évaluée au moment de la compilation même si l'expression n'est pas affectée à une variable constexpr. Que cela se produise ou non dépend de:

  • La complexité de l'expression
  • Le seuil que le compilateur utilise comme seuil lors de la tentative d'exécution de l'évaluation du temps de compilation
  • Autres heuristiques utilisées dans des cas particuliers (comme lorsque le clang élide les boucles)

Si une expression est explicitement fournie, comme dans le premier cas, elle a une complexité moindre et le compilateur est susceptible de l'évaluer au moment de la compilation.

De même, si une fonction est marquée en ligne, le compilateur est plus susceptible de l'évaluer au moment de la compilation, car les fonctions en ligne élèvent le seuil auquel l'évaluation peut avoir lieu.

Des niveaux d'optimisation plus élevés augmentent également ce seuil, comme dans l'exemple -Ofast, où toutes les expressions sont évaluées à true sur gcc en raison d'une évaluation plus précise au moment de la compilation.

Nous pouvons observer ce comportement ici sur l'explorateur du compilateur. Lorsqu'elle est compilée avec -O1, seule la fonction marquée en ligne est évaluée au moment de la compilation, mais à -O3 les deux fonctions sont évaluées au moment de la compilation.

NB: Dans les exemples du compilateur-explorateur, j'utilise à la printfplace iostream car il réduit la complexité de la fonction principale, rendant l'effet plus visible.

Démontrer que cela inlinen'affecte pas l'évaluation d'exécution

Nous pouvons nous assurer qu'aucune des expressions n'est évaluée au moment de la compilation en obtenant la valeur de l'entrée standard, et lorsque nous faisons cela, les 3 expressions renvoient false comme démontré ici: https://ideone.com/QZbv6X

#include <cmath>
#include <iostream>

bool is_cube(double r)
{
    return floor(cbrt(r)) == cbrt(r);
}
 
bool inline is_cube_inline(double r)
{
    return floor(cbrt(r)) == cbrt(r);
}

int main()
{
    double value;
    std::cin >> value;
    std::cout << (floor(cbrt(value)) == cbrt(value)) << std::endl; // false
    std::cout << (is_cube(value)) << std::endl; // false
    std::cout << (is_cube_inline(value)) << std::endl; // false
}

Contrairement à cet exemple , où nous utilisons les mêmes paramètres du compilateur mais fournissons la valeur au moment de la compilation, ce qui entraîne une évaluation plus précise au moment de la compilation.

J. Antonio Perez
la source
22

Comme observé, l'utilisation de l' ==opérateur pour comparer des valeurs en virgule flottante a donné lieu à différentes sorties avec différents compilateurs et à différents niveaux d'optimisation.

Un bon moyen de comparer les valeurs en virgule flottante est le test de tolérance relative décrit dans l'article: Les tolérances en virgule flottante revisitées .

Nous calculons d'abord la valeur Epsilon(de la tolérance relative ) qui dans ce cas serait:

double Epsilon = std::max(std::cbrt(r), std::floor(std::cbrt(r))) * std::numeric_limits<double>::epsilon();

Et puis utilisez-le dans les fonctions en ligne et non en ligne de cette manière:

return (std::fabs(std::floor(std::cbrt(r)) - std::cbrt(r)) < Epsilon);

Les fonctions sont maintenant:

bool is_cube(double r)
{
    double Epsilon = std::max(std::cbrt(r), std::floor(std::cbrt(r))) * std::numeric_limits<double>::epsilon();    
    return (std::fabs(std::floor(std::cbrt(r)) - std::cbrt(r)) < Epsilon);
}

bool inline is_cube_inline(double r)
{
    double Epsilon = std::max(std::cbrt(r), std::floor(std::cbrt(r))) * std::numeric_limits<double>::epsilon();
    return (std::fabs(std::round(std::cbrt(r)) - std::cbrt(r)) < Epsilon);
}

Maintenant, la sortie sera comme prévu ( [1 1 1]) avec différents compilateurs et à différents niveaux d'optimisation.

Démo en direct

PW
la source
Quel est le but de l' max()appel? Par définition, floor(x)est inférieur ou égal à x, donc max(x, floor(x))sera toujours égal x.
Ken Thomases
@KenThomases: Dans ce cas particulier, où un argument à maxest juste le floorde l'autre, il n'est pas obligatoire. Mais j'ai considéré un cas général où les arguments à maxpeuvent être des valeurs ou des expressions indépendantes les unes des autres.
PW
Ne devrait pas operator==(double, double)faire exactement cela, vérifier si la différence est plus petite qu'un epsilon à l'échelle? Environ 90% des questions en virgule flottante sur SO n'existeraient pas alors.
Peter - Réintègre Monica le
Je pense qu'il est préférable que l'utilisateur spécifie la Epsilonvaleur en fonction de ses besoins particuliers.
PW