long double (GCC spécifique) et float128
Je cherche des informations détaillées sur long double
et __float128
dans GCC/x86 (plus par curiosité qu'à cause d'un problème réel).
Peu de gens auront probablement besoin de ceux-ci (j'ai juste, pour la première fois, vraiment besoin d'un double
), mais je suppose qu'il vaut toujours la peine (et intéressant) de savoir ce que vous avez dans votre boîte à outils et de quoi il s'agit.
Dans cette optique, veuillez excuser mes questions un peu ouvertes:
- Quelqu'un pourrait-il expliquer le justification de la mise en œuvre et utilisation prévue de ces types, également en comparaison les uns des autres? Par exemple, sont-ils des "implémentations embarrassantes" parce que la norme autorise le type, et quelqu'un peut se plaindre s'ils ont juste la même précision que
double
, ou sont-ils destinés à être des types de première classe? - alternativement, quelqu'un a-t-il une bonne référence Web utilisable à partager? Une recherche Google sur
"long double" site:gcc.gnu.org/onlinedocs
ne m'a pas donné beaucoup de choses vraiment utiles. - en supposant que le commun mantra "Si vous croyez que vous avez besoin de double, vous ne comprenez probablement pas le point flottant" ne s'applique pas, c'est-à-dire que vous avez vraiment besoin de plus de précision que
float
, et on se fiche de savoir si 8 ou 16 octets de mémoire sont brûlés... est-il raisonnable de s'attendre à ce que l'on puisse aussi bien passer àlong double
ou__float128
au lieu dedouble
sans impact significatif sur les performances? - la fonctionnalité" précision étendue " des processeurs Intel a toujours été source de mauvaises surprises lorsque les valeurs ont été déplacées entre la mémoire et les registres. Si en fait 96 bits sont stockés, le type
long double
devrait éliminer ce problème. D'un autre côté, je comprends que le typelong double
est mutuellement exclusif avec-mfpmath=sse
, car il n'y a pas de "précision étendue" dans SSE.__float128
, d'autre part, devrait fonctionner parfaitement avec SSE math (bien qu'en l'absence d'instructions de précision quad certainement pas sur une base d'instructions 1:1). Suis-je le droit dans ces hypothèses?
(3. et 4. peut probablement être compris avec un peu de travail consacré au profilage et au démontage, mais peut-être que quelqu'un d'autre avait la même pensée auparavant et a déjà fait ce travail.)
Contexte (ceci est la partie TL;DR):
J'ai d'abord trébuché sur long double
parce que je regardais DBL_MAX
dans <float.h>
, et incidentiellement LDBL_MAX
est sur la ligne suivante. "Oh regardez, GCC a en fait des doubles de bits 128, pas que j'en ai besoin,mais ... cool " était ma première pensée. Surprise, Surprise: sizeof(long double)
retours 12... attends, tu veux dire 16?
Les standards C et c++ sans surprise ne donnent pas une définition très concrète du type. C99 (6.2.5 10) dit que les nombres de double
sont un sous-ensemble de long double
alors que C++03 unis (3.9.1 8) long double
a au moins autant de précision que double
(qui est la même chose, seulement libellée différemment). Fondamentalement, les normes laissent tout à la mise en œuvre, de la même manière qu'avec long
, int
, et short
.
Wikipedia dit que GCC utilise "précision étendue sur 80 bits sur les processeurs x86 quel que soit le stockage physique utilisé".
La documentation GCC indique, toutes sur la même page, que la taille du type est de 96 bits à cause de l'ABI i386, mais pas plus de 80 bits de précision sont activés par n'importe quelle option (hein? Comment?), aussi Pentium et les processeurs plus récents veulent qu'ils soient alignés en tant que nombres de bits 128. Ceci est la valeur par défaut sous 64 bits et peut être activé manuellement sous 32 bits, résultant en 32 bits de zéro padding.
Temps D'exécution d'un test:
#include <stdio.h>
#include <cfloat>
int main()
{
#ifdef USE_FLOAT128
typedef __float128 long_double_t;
#else
typedef long double long_double_t;
#endif
long_double_t ld;
int* i = (int*) &ld;
i[0] = i[1] = i[2] = i[3] = 0xdeadbeef;
for(ld = 0.0000000000000001; ld < LDBL_MAX; ld *= 1.0000001)
printf("%08x-%08x-%08x-%08xr", i[0], i[1], i[2], i[3]);
return 0;
}
La sortie, lors de l'utilisation de long double
, ressemble un peu à ceci, les chiffres marqués étant constants, et tous les autres finissent par changer à mesure que les nombres deviennent de plus en plus grands:
5636666b-c03ef3e0-00223fd8-deadbeef
^^ ^^^^^^^^
Cela suggère que c'est pas {[73] } un nombre de 80 bits. Un nombre de 80 bits a 18 chiffres hexadécimaux. Je vois 22 chiffres hexadécimaux changer, ce qui ressemble beaucoup plus à un nombre de 96 bits (24 chiffres hexadécimaux). Ce n'est pas non plus un nombre de 128 bits puisque 0xdeadbeef
n'est pas touché, ce qui est cohérent avec sizeof
renvoyant 12.
La sortie de __int128
ressemble à un nombre de 128 bits. Tous les bits finissent par basculer.
Compiler avec -m128bit-long-double
ne pas aligner long double
à 128 bits avec un remplissage zéro de 32 bits, comme indiqué par la documentation. Il n'utilise pas non plus __int128
, mais semble en effet s'aligner sur 128 bits, en remplissant avec la valeur 0x7ffdd000
(?!).
En Outre, LDBL_MAX
, semble fonctionner comme +inf
pour les deux long double
et __float128
. L'ajout ou la la soustraction d'un nombre comme 1.0E100
ou 1.0E2000
vers / depuis {[18] } entraîne le même motif de bits.
Jusqu'à présent, je croyais que les constantes foo_MAX
devaient contenir le plus grand nombre représentable qui n'est pas +inf
( apparemment ce n'est pas le cas?). Je ne sais pas non plus comment un nombre de 80 bits pourrait éventuellement agir comme +inf
pour une valeur de 128 bits... peut-être que je suis trop fatigué à la fin de la journée et que j'ai fait quelque chose de mal.
4 réponses
Ad 1.
Ces types sont conçus pour fonctionner avec des nombres avec une plage dynamique énorme. Le long double est implémenté de manière native dans le FPU x87. Le double 128b je soupçonne serait implémenté en mode logiciel sur x86s modernes, car il n'y a pas de matériel pour faire les calculs dans le matériel.
Le plus drôle est qu'il est assez courant de faire beaucoup d'opérations en virgule flottante dans une rangée et les résultats intermédiaires ne sont pas réellement stockés dans des variables déclarées mais plutôt stockés dans FPU registres profitant de la précision totale. C'est pourquoi comparaison:
double x = sin(0); if (x == sin(0)) printf("Equal!");
N'est pas sûr et ne peut pas être garanti pour fonctionner (sans commutateurs supplémentaires).
Ad. 3.
Il y a un impact sur la vitesse en fonction de la précision que vous utilisez. Vous pouvez modifier la précision utilisée de la FPU en utilisant:
void
set_fpu (unsigned int mode)
{
asm ("fldcw %0" : : "m" (*&mode));
}
Ce sera plus rapide pour les variables plus courtes, plus lent pour plus longtemps. Les doubles 128bit seront probablement effectués dans le logiciel, ce qui sera beaucoup plus lent.
Il ne s'agit pas seulement de RAM mémoire gaspillée, c'est à propos du cache gaspillé. Aller à 80 bit double de 64b double gaspillera de 33% (32b) à près de 50% (64b) de la mémoire (y compris le cache).
Ad 4.
D'autre part, je comprends que le type long double est mutuellement exclusif avec-mfpmath=sse, car il n'y a pas de " étendu la précision" dans l'ESS. __float128, d'autre part, devrait fonctionner parfaitement bien avec SSE math (bien qu'en l'absence de précision quad instruction certainement pas sur une base d'instruction 1:1). Je suis juste en dessous ces hypothèses?
Les unités FPU et SSE sont totalement séparées. Vous pouvez écrire du code en utilisant FPU en même temps que SSE. La question Est de savoir ce que le compilateur générera si vous le contraignez à utiliser uniquement SSE? Va-t-il essayer d'utiliser FPU de toute façon? J'ai fait de la programmation avec SSE et GCC ne générera qu'un seul SISD seul. Vous devez l'aider à utiliser les versions SIMD. __float128 fonctionnera probablement sur chaque machine, même L'AVR 8 bits uC. C'est juste jouer avec des morceaux après tout.
Le bit 80 dans la représentation hexadécimale est en fait 20 chiffres hexadécimaux. Peut-être que les bits qui ne sont pas utilisés proviennent d'une ancienne opération? Sur ma machine, j'ai compilé votre code et seulement 20 bits changent en long mode: 66b4e0d2-ec09c1d5-00007ffe-deadbeef
La version 128 bits a tous les bits qui changent. En regardant le objdump
, il semble qu'il utilisait l'émulation logicielle, il n'y a presque pas d'instructions FPU.
En outre, LDBL_MAX, semble fonctionner comme + inf pour les doubles longs et __float128. L'ajout ou la soustraction d'un nombre comme 1.0E100 ou 1.0E2000 à / de LDBL_MAX entraîne le même motif de bits. Jusqu'à maintenant, c'était mon croyance que les constantes foo_MAX devaient contenir la plus grande représentable nombre qui n'est pas +inf (apparemment ce n'est pas le cas?).
Cela semble étrange...
Je ne suis pas tout à fait sûr de la façon dont un nombre de 80 bits pourrait éventuellement agir comme + inf pour une valeur de 128 bits... peut être que je suis trop fatigué à la fin de la journée et ont fait quelque chose de mal.
Il est probablement en cours d'extension. Le motif qui est reconnu comme étant + inf en 80 bits est également traduit en + inf en flottant 128 bits.
IEEE-754 a défini 32 et 64 représentations à virgule flottante dans le but d'un stockage de données efficace, et une représentation de 80 bits dans le but d'un calcul efficace. L'intention était que given float f1,f2; double d1,d2;
une instruction comme d1=f1+f2+d2;
serait exécutée en convertissant les arguments en valeurs à virgule flottante de 80 bits, en les ajoutant et en convertissant le résultat en un type à virgule flottante de 64 bits. Cela offrirait trois avantages par rapport à l'exécution d'opérations sur d'autres types à virgule flottante directement:
Alors que du code ou des circuits séparés seraient nécessaires pour les conversions vers / depuis les types 32 bits et les types 64 bits, il ne serait nécessaire que d'avoir une seule implémentation "add", une implémentation "multiply", une implémentation "racine carrée", etc.
Bien que dans de rares cas, l'utilisation d'un type de calcul 80 bits puisse donner des résultats très légèrement moins précis que l'utilisation directe d'autres types (l'erreur d'arrondi du pire des cas est de 513 / 1024ulp dans cas où les calculs sur d'autres types donneraient une erreur de 511 / 1024ulp), les calculs chaînés utilisant des types de 80 bits seraient souvent plus précis-parfois beaucoup plus précis-que les calculs utilisant d'autres types.
Sur un système sans FPU, séparer un
double
en un exposant et une mantisse séparés avant d'effectuer des calculs, normaliser une mantisse et convertir une mantisse et un exposant séparés en undouble
, prend un peu de temps. Si l' le résultat d'un calcul sera utilisé comme entrée dans un autre et ignoré, l'utilisation d'un type de 80 bits décompressé permettra d'omettre ces étapes.
Pour que cette approche des mathématiques à virgule flottante soit utile, cependant, il est impératif que le code puisse stocker des résultats intermédiaires avec la même précision que celle utilisée dans le calcul, de sorte que temp = d1+d2; d4=temp+d3;
donne le même résultat que d4=d1+d2+d3;
. D'après ce que je peux dire, le but de long double
était de être que type. Malheureusement, même si K & R a conçu C de sorte que toutes les valeurs à virgule flottante soient transmises aux méthodes variadiques de la même manière, ANSI C a cassé cela. En C tel que conçu à l'origine, étant donné le code float v1,v2; ... printf("%12.6f", v1+v2);
, la méthode printf
n'aurait pas à se soucier de savoir si v1+v2
donnerait un float
ou un double
, puisque le résultat serait contraint à un type connu indépendamment. De plus, même si le type de v1
ou v2
changeait en double
, l'instruction printf
n'aurait pas à changer.
ANSI C, cependant, nécessite que le code qui appelle {[8] } doit savoir quels arguments sont double
et lesquels sont long double
; beaucoup de code-sinon une majorité-de code qui utilise long double
mais a été écrit sur des plates-formes où il est synonyme de double
ne parvient pas à utiliser les spécificateurs de format corrects pour les valeurs long double
. Plutôt que d'avoir long double
un type de 80 bits sauf lorsqu'il est passé en tant qu'argument de méthode variadique, auquel cas il serait contraint à 64 bits, de nombreux compilateurs ont décidé de faire long double
être synonyme de double
et non offrir tout moyen de stocker les résultats des calculs intermédiaires. Puisque l'utilisation d'un type de précision étendu pour le calcul n'est bonne que si ce type est mis à la disposition du programmeur, beaucoup de gens en sont venus à conclure que la précision étendue est mauvaise même si c'était seulement l'échec D'ANSI C à gérer les arguments variadiques qui l'a rendu problématique.
PS--l'usage prévu de long double
aurait bénéficié s'il y avait aussi eu un long float
qui était défini comme le type auquel float
les arguments pourraient être plus efficacement promus; sur de nombreuses machines sans unités à virgule flottante qui seraient probablement un type 48 bits, mais la taille optimale pourrait aller de 32 bits (sur les machines avec un FPU qui fait directement des mathématiques 32 bits) à 80 (sur les machines qui utilisent la conception envisagée par IEEE-754). Trop tard maintenant, cependant.
Il se résume à la différence entre 4.9999999999999999999 et 5.0.
- Bien que la plage soit la principale différence, c'est la précision qui est importante.
- ce type de données sera nécessaire dans les calculs de grands cercles ou les mathématiques de coordonnées qui sont susceptibles d'être utilisés avec les systèmes GPS.
- comme la précision est bien meilleure que la normale double, cela signifie que vous pouvez conserver typiquement 18 chiffres significatifs sans perdre la précision dans les calculs.
- étendue la précision, je crois, utilise 80 bits (utilisés principalement dans les processeurs mathématiques), donc 128 bits seront beaucoup plus précis.
C99 et c++11 ont ajouté des types float_t
et double_t
qui sont des alias pour les types à virgule flottante intégrés. En gros, float_t
est le type du résultat de faire de l'arithmétique entre les valeurs de type float
, et double_t
est le type du résultat de faire de l'arithmétique entre les valeurs de type double
.