52 votes

Existe-t-il un document décrivant comment Clang gère les excès de précision en virgule flottante?

Il est presque impossible(*) pour fournir strictes de la norme IEEE 754 sémantique à un coût raisonnable lorsque les seules instructions en virgule flottante est autorisé à utilisé sont les 387. Il est particulièrement difficile lorsque l'on souhaite garder la FPU de travail sur la pleine 64 bits significande de sorte que l' long double type est disponible pour la précision étendue. L'habituel "solution" est de faire des calculs intermédiaires à la seule précision, et de se convertir à la moindre précision à plus ou moins bien défini occasions.

Les versions récentes de GCC gérer l'excès de précision dans les calculs intermédiaires selon l'interprétation énoncées par Joseph S. Myers en 2008 à la poste, GCC liste de diffusion. Cette description fait un programme compilé avec gcc -std=c99 -mno-sse2 -mfpmath=387 tout à fait prévisible, pour le dernier morceau, comme je le comprends. Et si par hasard il ne l'est pas, c'est un bug et il sera résolu: Joseph S. Myers", a déclaré l'intention dans son post est pour le rendre prévisible.

Est-il documenté comment Clang poignées excès de précision (dire lorsque l'option -mno-sse2 est utilisé), et où?

(*) EDIT: c'est de l'exagération. C'est un peu ennuyeux, mais pas difficile à émuler binary64 lorsque l'on est autorisé à configurer la FPU x87 à l'utilisation de 53 bits significande.


À la suite d'un commentaire par R.. ci-dessous, voici le journal d'une interaction courte de la mine avec la version la plus récente de Clang j'ai :

Hexa:~ $ clang -v
Apple clang version 4.1 (tags/Apple/clang-421.11.66) (based on LLVM 3.1svn)
Target: x86_64-apple-darwin12.4.0
Thread model: posix
Hexa:~ $ cat fem.c
#include <stdio.h>
#include <math.h>
#include <float.h>
#include <fenv.h>

double x;
double y = 2.0;
double z = 1.0;

int main(){
  x = y + z;
  printf("%d\n", (int) FLT_EVAL_METHOD);
}
Hexa:~ $ clang -std=c99 -mno-sse2 fem.c
Hexa:~ $ ./a.out 
0
Hexa:~ $ clang -std=c99 -mno-sse2 -S fem.c
Hexa:~ $ cat fem.s 
…
    movl    $0, %esi
    fldl    _y(%rip)
    fldl    _z(%rip)
    faddp   %st(1)
    movq    _x@GOTPCREL(%rip), %rax
    fstpl   (%rax)
…

15voto

Nominal Animal Points 7207

Cela ne répond pas à l'origine de poser la question, mais si vous êtes un programmeur qui travaille avec des problèmes similaires, cette réponse pourrait vous aider.

Je ne vois vraiment pas où la perception de la difficulté. Fournir strictes de la norme IEEE-754 binary64 sémantique, tout en étant limité à 80387 mathématiques à virgule flottante, et de la retenue de 80 bits de long double calcul, semble suivre bien spécifié C99 les règles de casting avec GCC-4.6.3 et clang-3.0 (basé sur LLVM 3.0).

Edité pour ajouter: Pourtant, Pascal Cuoq est correcte: ni gcc 4.6.3 ou clang-llvm-3.0 que l'application effective de ces règles correctement pour '387 mathématiques à virgule flottante. Si les options du compilateur, les règles sont correctement appliquées à des expressions évaluées au moment de la compilation, mais pas pour le moment de l'exécution des expressions. Il existe des solutions de contournement, énumérés après le saut ci-dessous.

Je ne simulation de dynamique moléculaire de code, et je suis très familier avec la répétabilité et la prévisibilité des exigences et aussi avec le désir de conserver un maximum de précision disponibles lorsque cela est possible, donc, je ne demande que je sais de quoi je parle ici. Cette réponse devrait montrer que les outils existent et sont simples à utiliser; les problèmes se posent de ne pas être au courant ou non à l'aide de ces outils.

(Un exemple préféré que j'aime, est le Kahan algorithme de sommation. Avec le C99 et bon casting (ajout de jette, par exemple Wikipédia exemple de code), pas de trucs ou extra variables temporaires sont nécessaires à tous. La mise en œuvre quel que soit le compilateur niveau d'optimisation, y compris à l' -O3 et -Ofast.)

C99 stipule de façon explicite (p. ex. dans 5.4.2.2) que le casting et l'affectation à la fois de supprimer toutes supplément de portée et de précision. Cela signifie que vous pouvez utiliser long double de l'arithmétique par la définition de vos variables temporaires utilisés pendant le calcul d' long double, aussi le moulage de votre entrée de variables de ce type, chaque fois qu'une norme IEEE-754 binary64 est nécessaire, il suffit de lancer pour un double.

Sur '387, le cast génère une affectation et une charge sur les deux compilateurs; ce n'correctement rond de 80 bits de la valeur à la norme IEEE-754 binary64. Ce coût est très raisonnable à mon avis. L'heure exacte dépend de l'architecture et du code environnant; en général, il est et peut être entrelacé avec d'autres code de réduire les coûts pour négligeable les niveaux. Lorsque MMX, SSE ou AVX sont disponibles, leurs registres sont séparés de l'80 bits 80387 registres, et le casting est généralement assuré par le déplacement de la valeur vers le MMX/SSE/AVX registre.

(Je préfère le code de production en vue d'utiliser une variable de type point, disons tempdouble ou, pour les variables temporaires, de sorte qu'il peut être défini soit double ou long double en fonction de l'architecture et de la vitesse/précision compromis désiré.)

En un mot:

Ne pas assumer (expression) est double de précision, tout simplement parce que toutes les variables et les constantes littérales sont. L'écrire comme (double)(expression) si vous souhaitez que le résultat en double de précision.

Cela s'applique à des expressions composées, aussi, et peut parfois conduire à manier les expressions avec de nombreux niveaux de jette.

Si vous avez expr1 et expr2 que vous souhaitez calculer à 80 bits de précision, mais aussi besoin que le produit de chaque arrondi à 64 bits, utilisez

long double  expr1;
long double  expr2;
double       product = (double)(expr1) * (double)(expr2);

Remarque, product est calculée comme le produit de deux valeurs de 64 bits; pas calculée à 80 bits de précision, puis arrondis à la baisse. Calculer le produit à 80 bits de précision, puis en arrondissant, serait

double       other = expr1 * expr2;

ou, en ajoutant descriptif jette que vous dire exactement ce qui se passe,

double       other = (double)((long double)(expr1) * (long double)(expr2));

Il devrait être évident que l' product et other diffèrent souvent.

Le C99 règles de conversion sont juste un autre outil, vous devez apprendre à manier, si vous travaillez avec un mélange de 32-bit/64-bit/80 bits/128 bits des valeurs à virgule flottante. Vraiment, vous rencontre exactement les mêmes problèmes si vous mélangez binary32 et binary64 flotteurs (float et double sur la plupart des architectures)!

Peut-être la réécriture de Pascal Cuoq du code relatif à l'exploration, à appliquer correctement les règles de casting, rend cela plus clair?

#include <stdio.h>

#define TEST(eq) printf("%-56s%s\n", "" # eq ":", (eq) ? "true" : "false")

int main(void)
{
    double d = 1.0 / 10.0;
    long double ld = 1.0L / 10.0L;

    printf("sizeof (double) = %d\n", (int)sizeof (double));
    printf("sizeof (long double) == %d\n", (int)sizeof (long double));

    printf("\nExpect true:\n");
    TEST(d == (double)(0.1));
    TEST(ld == (long double)(0.1L));
    TEST(d == (double)(1.0 / 10.0));
    TEST(ld == (long double)(1.0L / 10.0L));
    TEST(d == (double)(ld));
    TEST((double)(1.0L/10.0L) == (double)(0.1));
    TEST((long double)(1.0L/10.0L) == (long double)(0.1L));

    printf("\nExpect false:\n");
    TEST(d == ld);
    TEST((long double)(d) == ld);
    TEST(d == 0.1L);
    TEST(ld == 0.1);
    TEST(d == (long double)(1.0L / 10.0L));
    TEST(ld == (double)(1.0L / 10.0));

    return 0;
}

La sortie, à la fois avec GCC et clang, est

sizeof (double) = 8
sizeof (long double) == 12

Expect true:
d == (double)(0.1):                                     true
ld == (long double)(0.1L):                              true
d == (double)(1.0 / 10.0):                              true
ld == (long double)(1.0L / 10.0L):                      true
d == (double)(ld):                                      true
(double)(1.0L/10.0L) == (double)(0.1):                  true
(long double)(1.0L/10.0L) == (long double)(0.1L):       true

Expect false:
d == ld:                                                false
(long double)(d) == ld:                                 false
d == 0.1L:                                              false
ld == 0.1:                                              false
d == (long double)(1.0L / 10.0L):                       false
ld == (double)(1.0L / 10.0):                            false

sauf que les versions récentes de GCC promouvoir le côté droit de l' ld == 0.1 à long double première (c'est à dire à l' ld == 0.1L), rendement true, et que la SSE/AVX, long double est de 128 bits.

Pour le pur '387 tests, j'ai utilisé

gcc -W -Wall -m32 -mfpmath=387 -mno-sse ... test.c -o test
clang -W -Wall -m32 -mfpmath=387 -mno-sse ... test.c -o test

avec l'optimisation des différents drapeau combinaisons ..., y compris l' -fomit-frame-pointer, -O0, -O1, -O2, -O3, et -Os.

À l'aide de tous les autres drapeaux ou C99 compilateurs aboutissent au même résultat, à l'exception de long double de la taille (et ld == 1.0 pour les actuelles versions de GCC). Si vous rencontrez des différences, je serais très reconnaissant à entendre parler d'eux; j'ai peut-être besoin d'avertir mes utilisateurs de ces compilateurs (des versions de compilateur). Notez que Microsoft ne prend pas en charge le C99, de sorte qu'ils sont complètement inintéressant pour moi.


Pascal Cuoq apporte un intéressant problème dans le commentaire de la chaîne ci-dessous, que je n'ai pas reconnaître immédiatement.

Lors de l'évaluation d'une expression, à la fois GCC et clang avec -mfpmath=387 spécifier que toutes les expressions sont évaluées à l'aide de 80 bits de précision. Cela conduit par exemple

7491907632491941888 = 0x1.9fe2693112e14p+62 = 110011111111000100110100100110001000100101110000101000000000000
5698883734965350400 = 0x1.3c5a02407b71cp+62 = 100111100010110100000001001000000011110110111000111000000000000

7491907632491941888 * 5698883734965350400 = 42695510550671093541385598890357555200 = 100000000111101101101100110001101000010100100001011110111111111111110011000111000001011101010101100011000000000000000000000000

produisant des résultats incorrects, parce que la chaîne de ceux dans le milieu de la binaire résultat est tout simplement à la différence entre 53 et 64 bits mantissas (64 et 80 bits des nombres à virgule flottante, respectivement). Ainsi, alors que le résultat attendu est

42695510550671088819251326462451515392 = 0x1.00f6d98d0a42fp+125 = 100000000111101101101100110001101000010100100001011110000000000000000000000000000000000000000000000000000000000000000000000000

le résultat obtenu avec seulement -std=c99 -m32 -mno-sse -mfpmath=387 est

42695510550671098263984292201741942784 = 0x1.00f6d98d0a43p+125 = 100000000111101101101100110001101000010100100001100000000000000000000000000000000000000000000000000000000000000000000000000000

En théorie, vous devriez être en mesure de dire à gcc et clang pour faire respecter le bon C99 règles d'arrondi à l'aide d'options

-std=c99 -m32 -mno-sse -mfpmath=387 -ffloat-store -fexcess-precision=standard

Cependant, ceci n'affecte que les expressions le compilateur optimise, et ne semble pas résoudre le 387 manipulation. Si vous utilisez par exemple clang -O1 -std=c99 -m32 -mno-sse -mfpmath=387 -ffloat-store -fexcess-precision=standard test.c -o test && ./test avec test.c cours de Pascal Cuoq l'exemple du programme, vous obtiendrez le résultat correct conformément à la norme IEEE-754 règles, mais seulement parce que le compilateur optimise loin de l'expression, et non pas à l'aide de l'387.

Tout simplement, au lieu de l'informatique

(double)d1 * (double)d2

gcc et clang dire le '387 à calculer

(double)((long double)d1 * (long double)d2)

C'est, en effet, je crois que c'est un compilateur bug affectant à la fois la gcc 4.6.3 et clang-llvm-3.0, et facile à reproduire. (Pascal Cuoq points qu' FLT_EVAL_METHOD=2 moyen des opérations sur le double de la précision des arguments est toujours fait à précision étendue, mais je ne vois pas de toute saine raison, en plus d'avoir à réécrire des parties d' libm sur le '387 -- faire qu'en C99 et compte tenu de la norme IEEE-754 règles sont réalisables par le matériel! Après tout, le correct fonctionnement est facilement réalisable par le compilateur, en modifiant les 387 mot de contrôle pour correspondre à la précision de l'expression. Et, étant donné les options du compilateur qui doit forcer ce comportement -- -std=c99 -ffloat-store -fexcess-precision=standard -- aucun sens s' FLT_EVAL_METHOD=2 comportement est réellement désiré, il n'y a pas de rétro-compatibilité des questions, soit.) Il est important de noter que, compte tenu de la bonne drapeaux du compilateur, les expressions évaluées au moment de la compilation ne évaluée correctement, et que seules les expressions évaluées au moment de l'exécution obtenir des résultats incorrects.

La solution la plus simple, et le portable, est d'utiliser fesetround(FE_TOWARDZERO) (à partir de fenv.h) à l'année, tous les résultats vers zéro.

Dans certains cas, l'arrondi vers zéro peut aider avec la prévisibilité et les cas pathologiques. En particulier, pour des intervalles comme x = [0,1), arrondi vers zéro signifie que la limite supérieure n'est jamais atteint par le biais de l'arrondissement; important si vous évaluer par exemple par morceaux splines.

Pour les autres modes d'arrondi, vous avez besoin de contrôler l'387 directement au matériel.

Vous pouvez utiliser __FPU_SETCW() de #include <fpu_control.h>, ou ouvrez-code. Par exemple, precision.c:

#include <stdlib.h>
#include <stdio.h>
#include <limits.h>

#define FP387_NEAREST   0x0000
#define FP387_ZERO      0x0C00
#define FP387_UP        0x0800
#define FP387_DOWN      0x0400

#define FP387_SINGLE    0x0000
#define FP387_DOUBLE    0x0200
#define FP387_EXTENDED  0x0300

static inline void fp387(const unsigned short control)
{
    unsigned short cw = (control & 0x0F00) | 0x007f;
    __asm__ volatile ("fldcw %0" : : "m" (*&cw));
}

const char *bits(const double value)
{
    const unsigned char *const data = (const unsigned char *)&value;
    static char buffer[CHAR_BIT * sizeof value + 1];
    char       *p = buffer;
    size_t      i = CHAR_BIT * sizeof value;
    while (i-->0)
        *(p++) = '0' + !!(data[i / CHAR_BIT] & (1U << (i % CHAR_BIT)));
    *p = '\0';
    return (const char *)buffer;
}


int main(int argc, char *argv[])
{
    double  d1, d2;
    char    dummy;

    if (argc != 3) {
        fprintf(stderr, "\nUsage: %s 7491907632491941888 5698883734965350400\n\n", argv[0]);
        return EXIT_FAILURE;
    }

    if (sscanf(argv[1], " %lf %c", &d1, &dummy) != 1) {
        fprintf(stderr, "%s: Not a number.\n", argv[1]);
        return EXIT_FAILURE;
    }
    if (sscanf(argv[2], " %lf %c", &d2, &dummy) != 1) {
        fprintf(stderr, "%s: Not a number.\n", argv[2]);
        return EXIT_FAILURE;
    }

    printf("%s:\td1 = %.0f\n\t    %s in binary\n", argv[1], d1, bits(d1));
    printf("%s:\td2 = %.0f\n\t    %s in binary\n", argv[2], d2, bits(d2));

    printf("\nDefaults:\n");
    printf("Product = %.0f\n\t  %s in binary\n", d1 * d2, bits(d1 * d2));

    printf("\nExtended precision, rounding to nearest integer:\n");
    fp387(FP387_EXTENDED | FP387_NEAREST);
    printf("Product = %.0f\n\t  %s in binary\n", d1 * d2, bits(d1 * d2));

    printf("\nDouble precision, rounding to nearest integer:\n");
    fp387(FP387_DOUBLE | FP387_NEAREST);
    printf("Product = %.0f\n\t  %s in binary\n", d1 * d2, bits(d1 * d2));

    printf("\nExtended precision, rounding to zero:\n");
    fp387(FP387_EXTENDED | FP387_ZERO);
    printf("Product = %.0f\n\t  %s in binary\n", d1 * d2, bits(d1 * d2));

    printf("\nDouble precision, rounding to zero:\n");
    fp387(FP387_DOUBLE | FP387_ZERO);
    printf("Product = %.0f\n\t  %s in binary\n", d1 * d2, bits(d1 * d2));

    return 0;
}

À l'aide de clang-llvm-3.0 pour compiler et exécuter, j'obtiens des résultats corrects,

clang -std=c99 -m32 -mno-sse -mfpmath=387 -O3 -W -Wall precision.c -o precision
./precision 7491907632491941888 5698883734965350400

7491907632491941888:    d1 = 7491907632491941888
        0100001111011001111111100010011010010011000100010010111000010100 in binary
5698883734965350400:    d2 = 5698883734965350400
        0100001111010011110001011010000000100100000001111011011100011100 in binary

Defaults:
Product = 42695510550671098263984292201741942784
          0100011111000000000011110110110110011000110100001010010000110000 in binary

Extended precision, rounding to nearest integer:
Product = 42695510550671098263984292201741942784
          0100011111000000000011110110110110011000110100001010010000110000 in binary

Double precision, rounding to nearest integer:
Product = 42695510550671088819251326462451515392
          0100011111000000000011110110110110011000110100001010010000101111 in binary

Extended precision, rounding to zero:
Product = 42695510550671088819251326462451515392
          0100011111000000000011110110110110011000110100001010010000101111 in binary

Double precision, rounding to zero:
Product = 42695510550671088819251326462451515392
          0100011111000000000011110110110110011000110100001010010000101111 in binary

En d'autres termes, vous pouvez contourner les problèmes de compilateur en utilisant fp387() définir la précision et le mode d'arrondi.

L'inconvénient est que certaines bibliothèques de mathématiques (libm.a, libm.so) peut être écrit avec l'hypothèse que les résultats intermédiaires sont toujours calculés à 80 bits de précision. Au moins la bibliothèque C de GNU fpu_control.h sur x86_64 a le commentaire "libm exige de la précision étendue". Heureusement, vous pouvez prendre l '387 implémentations par exemple, d'une bibliothèque C de GNU, et les mettre en œuvre dans un fichier d'en-tête ou d'écrire un travail, libm, si vous avez besoin de l' math.h fonctionnalités; en fait, je pense que je pourrais être en mesure de les y aider.

5voto

Pascal Cuoq Points 39606

Pour mémoire, voici ce que j'ai trouvé par l'expérimentation. Le programme suivant montre les différents comportements lorsqu'il est compilé avec Clang:

#include <stdio.h>

int r1, r2, r3, r4, r5, r6, r7;

double ten = 10.0;

int main(int c, char **v)
{
  r1 = 0.1 == (1.0 / ten);
  r2 = 0.1 == (1.0 / 10.0);
  r3 = 0.1 == (double) (1.0 / ten);
  r4 = 0.1 == (double) (1.0 / 10.0);
  ten = 10.0;
  r5 = 0.1 == (1.0 / ten);
  r6 = 0.1 == (double) (1.0 / ten);
  r7 = ((double) 0.1) == (1.0 / 10.0);
  printf("r1=%d r2=%d r3=%d r4=%d r5=%d r6=%d r7=%d\n", r1, r2, r3, r4, r5, r6, r7);
}

Les résultats varient avec le niveau d'optimisation:

$ clang -v
Apple LLVM version 4.2 (clang-425.0.24) (based on LLVM 3.2svn)
$ clang -mno-sse2 -std=c99  t.c && ./a.out
r1=0 r2=1 r3=0 r4=1 r5=1 r6=0 r7=1
$ clang -mno-sse2 -std=c99 -O2  t.c && ./a.out
r1=0 r2=1 r3=0 r4=1 r5=1 r6=1 r7=1

Le casting (double) - ce qui différencie r5 et r6 à -O2 n'a pas d'effet à l' -O0 et pour les variables r3 et r4. Le résultat r1 est différent de r5 à tous les niveaux d'optimisation, alors que r6 ne se distingue de la r3 à -O2.

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