Je recherche des informations détaillées sur _long double
_ et ___float128
_ dans GCC/x86 (plus par curiosité qu'en raison d'un problème réel).
Peu de gens en auront probablement besoin (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 quelque peu ouvertes:
double
, ou sont-elles conçues comme des types de première classe?"long double" site:gcc.gnu.org/onlinedocs
_ ne m'a pas donné grand-chose de vraiment utile.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 de double
sans impact significatif sur les performances?long double
_ devrait éliminer ce problème. D'un autre côté, je comprends que le type _long 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 bien avec SSE mathématiques (bien qu'en l'absence d'instructions de précision quadruple, certainement pas sur une base d'instructions 1: 1). Ai-je raison dans ces hypothèses?(3. et 4. peuvent probablement être compris avec un peu de travail consacré au profilage et au démontage, mais peut-être que quelqu'un d'autre avait déjà pensé la même chose et a déjà fait ce travail .)
Contexte (c'est la partie TL; DR):
J'ai d'abord trébuché sur _long double
_ parce que je cherchais _DBL_MAX
_ dans _<float.h>
_, et accessoirement _LDBL_MAX
_ est sur la ligne suivante. "Oh, regardez, GCC a en fait 128 doubles, pas que j'en ai besoin, mais ... cool" a été ma première pensée. Surprise, surprise: sizeof(long double)
renvoie 12 ... attendez, vous voulez dire 16?
Sans surprise, les normes C et C++ 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
_ tandis que C++ 03 indique (3.9.1 8) que _long double
_ a au moins autant précision comme double
(qui est la même chose, mais formulée différemment). Fondamentalement, les normes laissent tout à l'implémentation, de la même manière qu'avec long
, int
et short
.
Wikipedia dit que GCC utilise "une précision étendue de 80 bits sur les processeurs x86 quel que soit le stockage physique utilisé" .
La documentation GCC indique, tous sur la même page, que la taille du type est de 96 bits à cause de l'i386 ABI, mais pas plus de 80 bits de précision sont activés par n'importe quelle option (hein? Quoi?), Également Pentium et plus récent les processeurs veulent qu'ils soient alignés sur 128 bits. Il s'agit de la valeur par défaut sous 64 bits et peut être activé manuellement sous 32 bits, ce qui donne 32 bits de remplissage nul.
Temps pour exécuter 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-%08x\r", i[0], i[1], i[2], i[3]);
return 0;
}
_
La sortie, lors de l'utilisation de _long double
_, ressemble un peu à ceci, avec les chiffres marqués constants, et tous les autres changent finalement à mesure que les nombres deviennent de plus en plus gros:
_5636666b-c03ef3e0-00223fd8-deadbeef
^^ ^^^^^^^^
_
Cela suggère que c'est pas 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 pour ___int128
_ ressemble vraiment à un nombre de 128 bits. Tous les bits finissent par basculer.
La compilation avec _-m128bit-long-double
_ ne pas aligne _long double
_ sur 128 bits avec un remplissage nul de 32 bits, comme indiqué par la documentation. Il n'utilise pas non plus ___int128
_, mais semble en effet s'aligner sur 128 bits, avec un remplissage avec la valeur _0x7ffdd000
_ (?!).
En outre, _LDBL_MAX
_, semble fonctionner comme _+inf
_ pour _long double
_ et ___float128
_. L'ajout ou la soustraction d'un nombre tel que _1.0E100
_ ou _1.0E2000
_ vers/depuis _LDBL_MAX
_ donne le même motif binaire.
Jusqu'à présent, je pensais que les constantes _foo_MAX
_ devaient contenir le plus grand nombre représentable qui soit pas _+inf
_ (apparemment, ce n'est pas le cas?). Je ne suis pas non plus sûr de savoir comment un nombre 80 bits pourrait agir comme _+inf
_ pour une valeur de 128 bits ... peut-être que je suis juste trop fatigué à la fin de la journée et que j'ai fait quelque chose de mal.
Annonce 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 suppose, serait implémenté en mode logiciel sur les x86 modernes, car il n'y a pas de matériel pour effectuer les calculs dans le matériel.
Ce qui est drôle, c'est qu'il est assez courant de faire de nombreuses opérations en virgule flottante d'affilé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 des registres FPU profitant d'une précision totale. C'est pourquoi la comparaison:
double x = sin(0); if (x == sin(0)) printf("Equal!");
N'est pas sûr et ne peut être garanti de fonctionner (sans interrupteurs supplémentaires).
Un d. 3.
Il y a un impact sur la vitesse selon la précision que vous utilisez. Vous pouvez modifier la précision utilisée du 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 128 bits seront probablement effectués dans le logiciel et seront donc beaucoup plus lents.
Il ne s'agit pas seulement de RAM mémoire gaspillée, il s'agit de gaspillage de cache. Passer à 80 bits double de 64b double gaspillera de 33% (32b) à près de 50% (64b) de la mémoire ( cache inclus).
Annonce 4.
D'un autre côté, je comprends que le type double long s'exclut mutuellement avec -mfpmath = sse, car il n'y a pas de "précision étendue" dans SSE. __float128, d'autre part, devrait fonctionner parfaitement bien avec SSE mathématiques (bien qu'en l'absence d'instructions de précision quadruple certainement pas sur une base d'instructions 1: 1). Ai-je raison dans ces hypothèses?
Le FPU et les unités 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? essayez 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 toutes les machines, même l'AVR uC 8 bits.
Le 80 bits en 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 mode long: 66b4e0d2-ec09c1d5-00007ffe-deadbeef
La version 128 bits a tous les bits changés. En regardant objdump
on dirait qu'il utilisait une émulation logicielle, il n'y a presque pas d'instructions FPU.
En outre, LDBL_MAX, semble fonctionner comme + inf pour le double long et le __float128. L'ajout ou la soustraction d'un nombre comme 1.0E100 ou 1.0E2000 vers/depuis LDBL_MAX entraîne le même modèle de bits. Jusqu'à présent, je pensais 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?).
Cela semble étrange ...
Je ne sais pas non plus comment un nombre 80 bits pourrait en théorie agir comme + inf pour une valeur 128 bits ... peut-être que je suis juste trop fatigué à la fin de la journée et que j'ai fait quelque chose de mal.
Il est probablement en cours d'extension. Le modèle qui est reconnu comme étant + inf en 80 bits est également traduit en + inf en float 128 bits.
IEEE-754 a défini 32 et 64 représentations à virgule flottante à des fins de stockage efficace des données et une représentation à 80 bits à des fins de calcul efficace. L'intention était que, étant donné 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 80 bits, en les ajoutant et en reconvertissant le résultat en un flottant 64 bits -type de point. Cela offrirait trois avantages par rapport à l'exécution directe d'opérations sur d'autres types à virgule flottante:
Bien qu'un code ou un circuit séparé soit requis pour les conversions vers/à partir de types 32 bits et 64 bits, il ne serait nécessaire d'avoir qu'une seule implémentation "ajouter", une implémentation "multiplier", 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 la plus défavorable est 513/1024ulp dans les cas où les calculs sur d'autres types donneraient une erreur de 511/1024ulp ), les calculs chaînés utilisant des types 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, la séparation d'un double
en un exposant et une mantisse séparés avant d'effectuer des calculs, de normaliser une mantisse et de convertir une mantisse et un exposant séparés en double
, prend un peu de temps. Si le résultat d'un calcul est utilisé comme entrée pour un autre et rejeté, l'utilisation d'un type 80 bits non compressé permettra d'omettre ces étapes.
Cependant, pour que cette approche des mathématiques à virgule flottante soit utile, il est impératif que le code puisse stocker les résultats intermédiaires avec la même précision que celle utilisée pour le calcul, de sorte que temp = d1+d2; d4=temp+d3;
Produira 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 ce type. Malheureusement, même si K&R a conçu C pour 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
Produirait un float
ou un double
, car le résultat serait malgré tout contraint à un type connu. De plus, même si le type de v1
Ou v2
Était changé en double
, l'instruction printf
n'aurait pas à changer.
ANSI C, cependant, requiert que le code qui appelle printf
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 plateformes où il est synonyme de double
ne parvient pas à utiliser les spécificateurs de format corrects pour long double
valeurs. Plutôt que d'avoir long double
De type 80 bits, sauf lorsqu'il est passé comme argument de méthode variadique, auquel cas il serait contraint à 64 bits, de nombreux compilateurs ont décidé de faire de long double
Synonyme de double
et n'offre aucun moyen de stocker les résultats des calculs intermédiaires. Étant donné que l'utilisation d'un type de précision étendue pour le calcul n'est bonne que si ce type est mis à la disposition du programmeur, de nombreuses personnes ont conclu que la précision étendue était mauvaise même si ce n'était que l'incapacité de l'ANSI C à gérer les arguments variadiques de manière sensée qui le rendait problématique.
PS - Le but recherché de long double
Aurait été avantageux s'il y avait également un long float
Qui était défini comme le type vers lequel les arguments float
pouvaient être promus le plus efficacement; sur de nombreuses machines sans unités à virgule flottante qui seraient probablement de type 48 bits, mais la taille optimale pourrait aller de 32 bits (sur les machines avec un FPU qui fait directement des calculs 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.
Types ajoutés à C99 et C++ 11 float_t
et double_t
qui sont des alias pour les types à virgule flottante intégrés. Grossièrement, float_t
est le type du résultat de l'exécution de l'arithmétique parmi les valeurs de type float
et double_t
est le type du résultat de l'exécution de l'arithmétique parmi les valeurs de type double
.