web-dev-qa-db-fra.com

long double (spécifique GCC) et __float128

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:

  1. Quelqu'un pourrait-il expliquer la justification de la mise en œuvre et l'utilisation prévue de ces types, également en comparaison les uns des autres? Par exemple, s'agit-il de "mises en œuvre embarrassantes" parce que la norme autorise le type, et quelqu'un pourrait se plaindre si elles ne sont que de la même précision que double, ou sont-elles conçues comme des types de première classe?
  2. 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é grand-chose de vraiment utile.
  3. En supposant que le mantra commun "si vous croyez que vous avez besoin du double, vous ne comprenez probablement pas le point flottant" ne s'applique pas, c'est-à-dire que vous vraiment a besoin de plus de précision que juste 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?
  4. La fonctionnalité de "précision étendue" des processeurs Intel a toujours été une source de mauvaises surprises lorsque les valeurs ont été déplacées entre la mémoire et les registres. Si en réalité 96 bits sont stockés, le type _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.

28
Damon

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.

21
Caladan

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:

  1. 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.

  2. 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.

  3. 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.

2
supercat

Il se résume à la différence entre 4.9999999999999999999 et 5.0.

  1. Bien que la plage soit la principale différence, c'est la précision qui est importante.
  2. Ce type de données sera nécessaire dans les calculs de grands cercles ou les mathématiques de coordonnées susceptibles d'être utilisées avec les systèmes GPS.
  3. Comme la précision est bien meilleure que le double normal, cela signifie que vous pouvez généralement conserver 18 chiffres significatifs sans perdre en précision dans les calculs.
  4. Une précision étendue utilise, je crois, 80 bits (utilisés principalement dans les processeurs mathématiques), donc 128 bits seront beaucoup plus précis.
1
R Telkman

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.

0
user3405743