Formes de constantes pour l’ajout et la multiplication de hautes performances pour le double

J’ai besoin d’append ou de multiplier efficacement certaines constantes dans un résultat de type double dans une boucle afin d’éviter tout débordement. Par exemple, si nous avons int, la multiplication par une puissance de 2 sera rapide car le compilateur utilisera le décalage de bits. Existe-t-il une forme de constantes pour une double addition et une multiplication efficaces?

Edit: Il semble que peu de gens comprennent ma question, excusez-moi de ma négligence. Je vais append du code. Si a est un int, ceci (en multipliant par une puissance de 2) sera plus efficace

 int a = 1; for(...) for(...) a *= somefunction() * 1024; 

que lorsque 1024 est remplacé par, par exemple, 1023. Je ne sais pas quel est le meilleur choix si nous voulons append un int, mais ce n’est pas de mon intérêt. Je suis intéressé par le cas où a est un double. Quelles sont les formes de constantes (par exemple puissance de 2) que l’on peut efficacement append et multiplier à un double? La constante est arbitraire , il faut juste qu’elle soit assez grande pour empêcher un sous-débordement.

Ce n’est probablement pas limité aux langages C et C ++, mais je ne connais pas de balise plus appropriée.

Sur la plupart des processeurs modernes, multiplier simplement par une puissance de deux (par exemple, x *= 0x1p10; multiplier par 2 10 ou x *= 0x1p-10; diviser par 2 10 ) sera rapide et sans erreur (sauf si le résultat est assez grand pour déborder ou assez petit pour déborder).

Certains processeurs ont des «sorties anticipées» pour certaines opérations en virgule flottante. C’est-à-dire qu’ils complètent l’instruction plus rapidement lorsque certains bits sont nuls ou répondent à d’autres critères. Toutefois, l’addition, la soustraction et la multiplication à virgule flottante s’exécutent généralement en environ quatre cycles de traitement, de sorte qu’elles sont assez rapides, même sans sorties prématurées. En outre, la plupart des processeurs modernes exécutent plusieurs instructions à la fois. Par conséquent, le travail se poursuit lorsqu’une multiplication est en cours et sont en pipeline. Ainsi, une multiplication peut généralement être démarrée (et une fin) à chaque cycle de la CPU. (Parfois plus.)

La multiplication par des puissances de deux n’a pas d’erreur d’arrondi car le significande (fraction de la valeur) ne change pas, le nouveau significande est donc parfaitement représentable. (Excepté, en multipliant par une valeur inférieure à 1, les bits du significande peuvent être poussés au-dessous de la limite du type à virgule flottante, entraînant un dépassement de capacité. Pour le double format commun IEEE 754, cela ne se produit que lorsque la valeur est inférieure à 0x1p-1022.)

N’utilisez pas la division pour la mise à l’échelle (ou pour inverser les effets d’une mise à l’échelle précédente). Au lieu de cela, multipliez par l’inverse. (Pour supprimer une mise à l’échelle précédente de 0x1p57, multipliez par 0x1p-57.) En effet, les instructions de division sont lentes sur la plupart des processeurs modernes. Par exemple, 30 cycles n’est pas inhabituel.

Commencez par obtenir votre double dans une union et sélectionnez les parties “plage” et “exposant” . Ensuite, déplacez uniquement les parties “exposant” ou “plage” . Recherchez les normes à virgule flottante IEEE. N’oubliez pas le signe et le dernier bit de mantisse .

 union int_add_to_double { double this_is_your_double_precision_float; struct your_bit_representation_of_double { int range_bit:53;//you can shift this to make range effect //i dont know which is mantissa bit. maybe it is first of range_bit. google it. int exponent_bit:10; //exponential effect int sign_bit:1; //take negative or positive }dont_forget_struct_name; }and_a_union_name; 

L’addition et la multiplication en virgule flottante prennent généralement peu de cycles dans les processeurs modernes.

Peut-être devriez-vous prendre du recul et réfléchir à ce que fait l’algorithme. Dans votre exemple, vous avez une double boucle nestede … cela signifie que “somefunction ()” peut être appelé plusieurs fois. La représentation commune de “double” est IEEE, qui utilise 11 bits pour l’exposant et 52 bits pour la mantisse (53 car en réalité, sauf pour zéro, il existe un “1” implicite). Cela signifie que vous pouvez représenter des nombres avec une précision de 53 bits dans une plage allant de très petits à très grands nombres – le “virgule flottante” binary peut se déplacer de 1024 (2 ^ 10) places à gauche ou à droite du nombre “1.0”. ..si “somefunction ()” est appelé mille fois et renvoie toujours un nombre inférieur ou égal à 0,5, vous êtes sous-alimenté (chaque fois que vous multipliez par 0,5, vous coupez votre nombre “a” en deux, ce qui signifie que vous déplacez le binary flottant. pointer vers la gauche. Sur x86, vous pouvez demander au processeur de “purger à zéro les valeurs normales” en définissant un bit dans un registre de contrôle – il n’existe pas d’interface de programmation portable permettant de faire cela, avec gcc vous utilisez

 _MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON); 

Dire au processeur de supprimer les valeurs normales à zéro accélérera l’exécution de votre code, car le processeur n’essaie pas de représenter des nombres supérieurs à la normale (inférieure à la normale) (sous-normales ou dénormales). Il semble que vous essayiez de maintenir la précision face à un algorithme qui produit des sous-normales (ce qui force la perte de précision). La meilleure façon de gérer cela dépend de savoir si vous contrôlez “somefunction ()” ou non. Si vous avez le contrôle de cette fonction, vous pouvez alors “normaliser” la valeur renvoyée à quelque chose dans la plage

 0.5 <= X <= 2.0 

En d'autres termes, renvoyez les valeurs centrées autour de 1.0 et gardez une trace séparée de la puissance de 2 dont vous avez besoin pour multiplier la réponse finale afin de la redimensionner correctement.

Si vous utilisez SSE, append des constantes directement dans le champ exposant est une astuce légitime (dans le code FPU, c’est assez terrible) – le débit est double et la latence est 4 fois meilleure (sauf sur les processeurs qui ont un float-> int et / ou int-> pénalité de flottement). Mais puisque vous ne faites que cela pour empêcher les dénormaux, pourquoi ne pas activer les zones FTZ (cadrer à zéro) et DAZ (les dénormaux sont nuls)?

Vous pouvez utiliser les fonctions standard frexp / ldexp qui décomposent les valeurs IEE 754 en leurs composants:

http://www.cplusplus.com/reference/clibrary/cmath/frexp/

http://www.cplusplus.com/reference/clibrary/cmath/ldexp/

Voici un exemple de code simple:

 #include  #include  int main () { double value = 5.4321; int exponent; double significand = frexp (value , &exponent); double result = ldexp (significand , exponent+1); std::cout << value << " -> " << result << "\n"; return 0; } 

L'exécution traite avec: http://ideone.com/r3GBy

Sur un processeur en gigahertz, vous pouvez économiser 1 ou 2 nanosecondes en optimisant cette méthode (décalage par rapport à l’arithmétique). Cependant, le temps nécessaire au chargement et au stockage de la mémoire est de l’ordre de 100 nsec et 10 ms au disque dur. S’inquiéter des opérations arithmétiques est inutile par rapport à l’optimisation de l’utilisation du cache et de l’activité du disque. Cela ne fera jamais une différence dans aucun programme de production réel.

Juste pour éviter tout malentendu, je ne dis pas que la différence est petite, alors ne vous inquiétez pas, je dis que c’est zéro. Vous ne pouvez pas écrire un programme simple dans lequel la différence de temps d’ALU ne se chevauche pas complètement avec le temps d’arrêt de la CPU en attente de mémoire ou d’E / S.