Pas de dépendances
#include <stdio.h> void testInt(int num,int denom) { double res = num/denom; printf("testIntInt : %.20f\n",res); } void testFloat(float num,int denom) { double res,res3,res4; float res2; res = num/denom; printf("testFloat 1 : %.20f\n",res); res2 = num/denom; printf("testFloat 2 : %.20f\n",res2); res3 = 10.0f/3; printf("testFloat 3 : %.20f\n",res3); res4 = 10.0f/3.0; printf("testFloat 4 : %.20f\n",res4); } void testDouble(double num,int denom) { double res,res3; float res2; res = num/denom; printf("testDouble 1 : %.20f\n",res); res2 = (float)(num/denom); // warning printf("testDouble 2 : %.20f\n",res2); res3 = 10.0/3; printf("testDouble 3 : %.20f\n",res3); } int main() { testInt(10,3); testFloat(10.0f,3); testDouble(10.0,3); return 0; }
Le C permet de faire des divisions entières, et des divisions flottantes. Nous allons voir quelques cas. Comme chacun sait, mathématiquement, 10 / 3 = 3.3333333333333333333333333333333...... une infinité de 3. testInt. ******** La fonction testInt donne deux int : 10 et 3 et les divise. Elle récupère le résultat dans un double. Pourtant, si on regarde le premier printf du programme, il donne 3.000000000000000000 (la syntaxe %.20f permet d'afficher 20 chiffres après la virgule). La raison est simple : d'abord, on a divisé deux int : donc on a divisé 10 par 3. Comme ce sont des int, la division entière a été invoquée, donc la résultat est un int qui vaut 3. Ensuite, et seulement ensuite, ce résultat est rangé dans le double res. Mais comme c'est un 3 (int) qui est rangé, donc converti, en double, alors le résultat donné est 3.000000000000000000 Pensez bien à cela : Ce qui détermine si une division sera entière ou non n'est pas le type de la variable de retour, mais bien le type des deux opérandes. Assembleur : ------------ Si votre débuggueur le permet, regardez le code assembleur généré. Sur un PC, l'instruction "idiv" a été invoquée : division ENTIERE. testFloat. ********* Vous le savez, un float prend moins de mémoire qu'un double. Donc aura moins de précision. Il y a donc plusieurs types de flottants. Cependant, le processeur lui, ne sait faire des divisions que sur des double. (des flottants de 8 octets, 64 bits) Il ne s'encombre pas avec des float. Qui peut le plus peut le moins. Il utilisera l'instruction "fdiv" ou "fdivr" pour diviser deux flottants. Il stockera dans son registre interne un flottant f1. fdiv(f2) permettre de calculer f1/f2 ; fdivr(f2) permettra de calculer f2/f1 (dans fdivr, le r veut dire reverse). Illustrons : testFloat1: ----------- testFloat1 prend un float et un int, et stocke le résultat dans un double. Voyons le code assembleur utilisé : voici une petite doc des mnemoniques assembleur : https://docs.oracle.com/cd/E19120-01/open.solaris/817-5477/eoizy/index.html res = num/denom; 003D147E fild dword ptr [denom] 003D1481 fdivr dword ptr [num] 003D1484 fstp qword ptr [res] fild charge le int (denom) de 4 octets (dword) dans le registre flottant (donc le convertit en flottant au passage). fdivr prend le float de 4 octets (dword) et le divise par celui stocké dans le registre, le résultat étant gardé dans le registre. Et c'est la que c'est intéressant : fstp va copier 8 octets (qword) dans res (car c'est un double, donc 8 octets. Donc malgré le fait qu'on a importé un int de 4 octets (fild dword), et multiplié par un float de 4 octets (fdivr dword), le calcul a pondu un flottant sous 8 octets, puisqu'on le récupère dans un double, et que le printf va nous prouver que le calcul a été fait en double précision. En effet, le printf donne : 3.33333333333333350000 Nous pouvons constater qu'on a 16 chiffres significatifs : le 3 avant la virgule, et 15 3 derrière. Au dela, le "50000" restant est totalement non-fiable, et certains autres processeurs pourront donner n'importe quoi d'autre. Nous avons atteint la limite du double, le bout de la mantisse... 16 chiffres significatifs, c'est donc un double. (nous preouverons juste après que c'est bien un double en voyant ce que donne un float) testFloat2: ----------- testFloat2 prend un float et un int, et stocke le résultat dans un float. Voyons le code assembleur utilisé : res2 = num/denom; 00FF14A7 fild dword ptr [denom] 00FF14AA fdivr dword ptr [num] 00FF14AD fstp dword ptr [res2] La seule différence est sur la troisième ligne : fstp va copier 4 octets (dword) au lieu de 8 auparavant. Il va donc enregistrer la résultat sous forme de float. Mais le calcul, lui, a été le même que testFloat1 !! Le printf nous donne : 3.33333325386047360000 Il n'y a que 7 chiffres significatifs ! Après, ça fait n'importe quoi ! C'est la limite du float ! testFloat3: ----------- dans ce test, je divise un float par un int, et je stocke le résultat dans un double. Donc comme testFloat1 ! Donc je m'attends au même résultat !! Horreur, c'est pas le même... On un vilain float au lieu du double du testFloat1. Regardons donc le code assembleur généré : res3 = 10.0f/3; 002D14D0 fld qword ptr [__real@400aaaaaa0000000 (2D57D0h)] 002D14D6 fstp qword ptr [res3] Nous voyons qu'il y a un fld (charge dans le registre interne un float de 8 octets (qword), donc un double). Il y a un fstp qui stocke un double dans res3 : tant mieux, car res3 est un double. Mais il n'Y A PAS DE DIVISION, pas de fdiv ni fdivr. Pourquoi ? Parce que le COMPILATEUR a vu que je donnais des constantes dans le code. Donc c'est lui qui a fait le calcul. Il a vu que c'était un float par un int : il a donc calculé un float ! Et ce float calculé, c'est __real@400aaaaaa0000000 (c'est son codage hexa) Ce float calculé par le compilateur a donc été 3.33333325386047360000 On le stocke ensuite dans un double. Mais le mal est fait : si je mets 3.33333325386047360000 dans un double, il va garder cette valeur. testFloat4: ----------- Cette fois ci, je divise un float par un double. Ce sont des constantes, donc le compilateur va calculer un double. D'ailleurs on le voit ici : res4 = 10.0f/3.0; 00FB14F9 fld qword ptr [__real@400aaaaaaaaaaaab (0FB5770h)] 00FB14FF fstp qword ptr [res4] Le nombre passé __real@400aaaaaaaaaaaab n'est pas le même qu'au dessus. Le printf m'affichera donc 16 chiffres significatifs. testDouble. *********** testDouble1: ------------ Ce test ressemble à testFloat1, sauf que je divise un double par un int, je mets le résultat dans un double. res = num/denom; 00C5155E fild dword ptr [denom] 00C51561 fdivr qword ptr [num] 00C51564 fstp qword ptr [res] La seule différence est que fdivr prend un qword comme opérande (il prend 8 octets, le double, car num est un double). J'ai donc bien mes 16 chiffres significatifs en retour. testDouble2: ------------ Cette fois ci, je divise un double par un int, et je mets le résultat dans un float. Vous constatez à la compilation qu'on se prend un warning ! C'est normal : on divise un double par un int, donc le compilo s'attend à ce qu'on veuille le résultat en double. Or, si on le stocke dans un float, il y a une perte de précision ! Mais pour le processeur, il n'y a pas de soucis : ce n'est pas la première fois qu'on voit un fstp dword. C'est vraiment le compilateur qui prévient. res2 = num/denom; // warning 00C51587 fild dword ptr [denom] 00C5158A fdivr qword ptr [num] 00C5158D fstp dword ptr [res2] On peut faire taire le compilateur en lui explicitant qu'on attend un float, par cast de cette manière : res2 = (float)(num/denom); // warning 00CC1587 fild dword ptr [denom] 00CC158A fdivr qword ptr [num] 00CC158D fstp dword ptr [res2] Vous pouvez constater que le code machine ne bouge pas ! Tout cela, c'est pour le compilateur et sa rigueur. Le processeur, lui, part du principe que vous savez ce que vous faites et fonce ! Evidemment, dans le résultat, on n'a que 7 chiffres significatifs puisque res2 est un float. testDouble3: ------------ Ici, on divise en dur un double par un int. Donc le compilo précalcule le double, et le range dans res3. Aucun calcul au niveau de l'exécution. res3 = 10.0/3; 00C515B0 fld qword ptr [__real@400aaaaaaaaaaaab (0C55770h)] 00C515B6 fstp qword ptr [res3]