Perte de précision avec DC

12

Je veux utiliser dcpour gérer certains nombres de base 16 avec des points hexadécimaux, mais je rencontre des problèmes de précision. Par exemple, ci-dessous, je multiplie F423F.FDpar 100, les deux hex. La réponse attendue est F423FFD, au lieu de cela, elle donne F423FFA.E1, proche mais pas assez précise même après l'arrondissement.

$ dc
16 d i o F423F.FD 100 * p
F423FFA.E1

J'ai lu que dcc'était une calculatrice de précision illimitée, et ce n'est en aucun cas un grand nombre. Y a-t-il quelque chose que je fais mal?

Merci pour vos réponses. Étant donné les problèmes avec dc, j'ai mordu la balle et j'ai écrit mon propre analyseur pour des nombres réels dans d'autres bases. Si quelqu'un s'intéresse au code, je peux le poster ici.

Yimin Rong
la source

Réponses:

6

Notez que l'impression du numéro d'origine montre qu'il est arrondi:

$ dc <<<'16 d i o F423F.FD p'
F423F.FA

Vous pouvez le contourner en ajoutant de nombreux zéros de fin pour plus de précision:

$ dc <<<'16 d i o F423F.FD000000 100 * p'
F423FFD.0000000
meuh
la source
Merci. Je pense que cela finira par prendre plus de code pour masser les chiffres dcà utiliser puis pour écrire directement un analyseur! (L'entrée peut avoir ou non une décimale, et peut être dans d'autres bases, donc la quantité de remplissage varie.)
Yimin Rong
2
Je vais marquer cela comme la réponse acceptée. Les personnes responsables de la maintenance ont dcrépondu: Pour gérer correctement les chiffres fractionnaires non décimaux, il faudrait un modèle complètement différent du modèle à échelle décimale utilisé par dc et bc (comme dicté par POSIX pour bc et par la tradition historique pour les deux). , donc techniquement, il pourrait être corrigé dc, mais cela casserait probablement bc, donc classé comme WONTFIX.
Yimin Rong
8

Exprimé en décimal (en utilisant dcpour convertir), cela correspond à 999999,98 (arrondi vers le bas) × 256, soit 255999994.88, qui est F423FFA.E1 en hexadécimal.

La différence vient donc du dccomportement d'arrondi de: au lieu de calculer 256 × (999999 + 253 ÷ 256), ce qui donnerait 255999997, il arrondit 253 ÷ 256 vers le bas et multiplie le résultat.

dcest une calculatrice de précision arbitraire , ce qui signifie qu'il peut calculer avec la précision que vous voulez, mais vous devez lui dire ce que c'est. Par défaut, sa précision est 0, ce qui signifie que la division produit uniquement des valeurs entières et que la multiplication utilise le nombre de chiffres dans l'entrée. Pour définir la précision, utilisez k(et gardez à l'esprit que la précision est toujours exprimée en chiffres décimaux, quel que soit le radix d'entrée ou de sortie):

10 k
16 d i o
F423FFD 100 / p
F423F.FD0000000
100 * p
F423FFD.000000000

(Une précision de 8 chiffres serait suffisante car c'est ce dont vous avez besoin pour représenter 1 ÷ 256 en décimal.)

Stephen Kitt
la source
1
Cela semblerait être un résultat complètement inattendu pour une calculatrice à "précision arbitraire"?
Yimin Rong
3
Il perd toujours de la précision lorsqu'il kest défini: 10 k 16 d i o F423F.FD pF423F.FA, donc je devrais augmenter tous les nombres avant de les utiliser dc. Fondamentalement, cela revient à les pré-analyser de toute façon.
Yimin Rong
2
@Yimin oui, malheureusement, met à l' dcéchelle son entrée en utilisant uniquement le nombre de chiffres, ce qui me semble être un bug (car le nombre de chiffres est calculé en utilisant le radix d'entrée, mais appliqué à la valeur décimale).
Stephen Kitt
1
@dhag c'est ce que POSIX spécifie (pour bc, qui dcest basé sur): "Les calculs internes doivent être effectués comme s'ils étaient en décimal, quelles que soient les bases d'entrée et de sortie, jusqu'au nombre spécifié de chiffres décimaux."
Stephen Kitt
1
C'est vraiment un problème de la façon dont une constante est analysée. Essayez 20 k 16 d i o 0.3 1 / p (qui imprime .1999999999999999999). Comprenez que l'opération ne fait que diviser 0.2par 1(ce qui en théorie ne devrait pas changer la valeur). Tandis que 20 k 16 d i o 0.3000 1 / p(correctement) s'imprime .30000000000000000. (Suite)
NotAnUnixNazi
1

Le problème

Le problème est la manière dont dc (et bc) comprennent les constantes numériques.
Par exemple, la valeur (en hexadécimal) 0.3(divisée par 1) est transformée en une valeur proche de0.2

$ dc <<<"20k 16 d i o 0.3 1 / p"
.199999999999999999999999999

En fait, la constante simple est 0.3également modifiée:

$ dc <<<"20 k 16 d i o     0.3     p"
.1

Il semble que ce soit d'une manière étrange, mais ce n'est pas le cas (plus tard).
L'ajout de zéros permet à la réponse d'approcher la valeur correcte:

$ dc <<<"20 k 16 d i o     0.30     p"
.2E

$ dc <<<"20 k 16 d i o     0.300     p"
.2FD

$ dc <<<"20 k 16 d i o     0.3000     p"
.3000

La dernière valeur est exacte et restera exacte, quel que soit le nombre de zéros ajoutés.

$ dc <<<"20 k 16 d i o     0.30000000     p"
.3000000

Le problème est également présent dans bc:

$ bc <<< "scale=20; obase=16; ibase=16;    0.3 / 1"
.19999999999999999

$ bc <<< "scale=20; obase=16; ibase=16;    0.30 / 1"
.2E147AE147AE147AE

$ bc <<< "scale=20; obase=16; ibase=16;    0.300 / 1"
.2FDF3B645A1CAC083

$ bc <<< "scale=20; obase=16; ibase=16;    0.3000 / 1"
.30000000000000000

Un chiffre par bit?

Le fait très intuitif pour les nombres à virgule flottante est que le nombre de chiffres requis (après le point) est égal au nombre de bits binaires (également après le point). Un nombre binaire 0,101 est exactement égal à 0,625 en décimal. Le nombre binaire 0,0001110001 est (exactement) égal à 0.1103515625(dix chiffres décimaux)

$ bc <<<'scale=30;obase=10;ibase=2; 0.101/1; 0.0001110001/1'; echo ".1234567890"
.625000000000000000000000000000
.110351562500000000000000000000
.1234567890

De plus, pour un nombre à virgule flottante comme 2 ^ (- 10), qui en binaire n'a qu'un seul bit (défini):

$ bc <<<"scale=20; a=2^-10; obase=2;a; obase=10; a"
.0000000001000000000000000000000000000000000000000000000000000000000
.00097656250000000000

Formé du même nombre de chiffres binaires .0000000001(10) que de chiffres décimaux .0009765625(10). Ce n'est peut-être pas le cas dans d'autres bases, mais la base 10 est la représentation interne des nombres en dc et en bc et est donc la seule base dont nous devons vraiment nous préoccuper.

La preuve mathématique se trouve à la fin de cette réponse.

échelle bc

Le nombre de chiffres après le point peut être compté avec la scale()forme de fonction intégrée bc:

$ bc <<<'obase=16;ibase=16; a=0.FD; scale(a); a; a*100'
2
.FA
FA.E1

Comme indiqué, 2 chiffres sont insuffisants pour représenter la constante 0.FD.

De plus, le simple fait de compter le nombre de caractères utilisés après le point est une manière très incorrecte de signaler (et d'utiliser) l'échelle du nombre. L'échelle d'un nombre (dans n'importe quelle base) doit calculer le nombre de bits nécessaires.

Chiffres binaires dans un flotteur hexadécimal.

Comme on le sait, chaque chiffre hexadécimal utilise 4 bits. Par conséquent, chaque chiffre hexadécimal après le point décimal nécessite 4 chiffres binaires, qui en raison du fait (impair?) Ci-dessus nécessitent également 4 chiffres décimaux.

Par conséquent, un nombre comme 0.FDnécessitera 8 chiffres décimaux pour être représenté correctement:

$ bc <<<'obase=10;ibase=16;a=0.FD000000; scale(a);a;a*100'
8
.98828125
253.00000000

Ajouter des zéros

Le calcul est simple (pour les nombres hexadécimaux):

  • Comptez le nombre de chiffres hexadécimaux ( h) après le point.
  • Multipliez hpar 4.
  • Ajoutez des h×4 - h = h × (4-1) = h × 3 = 3×hzéros.

En code shell (pour sh):

a=F423F.FD
h=${a##*.}
h=${#h}
a=$a$(printf '%0*d' $((3*h)) 0)
echo "$a"

echo "obase=16;ibase=16;$a*100" | bc

echo "20 k 16 d i o $a 100 * p" | dc

Qui s'imprimera (correctement en dc et en bc):

$  sh ./script
F423F.FD000000
F423FFD.0000000
F423FFD.0000000

En interne, bc (ou dc) pourrait faire correspondre le nombre de chiffres requis au nombre calculé ci-dessus ( 3*h) pour convertir les flottants hexadécimaux en représentation décimale interne. Ou une autre fonction pour d'autres bases (en supposant que le nombre de chiffres est fini par rapport à la base 10 (interne de bc et dc) dans cette autre base). Comme 2 i (2,4,8,16, ...) et 5,10.

posix

La spécification posix indique que (pour bc, sur lequel dc est basé):

Les calculs internes doivent être effectués comme en décimal, quelles que soient les bases d'entrée et de sortie, au nombre spécifié de chiffres décimaux.

Mais "... le nombre spécifié de chiffres décimaux." pourrait être compris comme "… le nombre nécessaire de chiffres décimaux pour représenter la constante numérique" (comme décrit ci-dessus) sans affecter les "calculs internes décimaux"

Car:

bc <<<'scale=50;obase=16;ibase=16; a=0.FD; a+1'
1.FA

bc n'utilise pas vraiment 50 ("le nombre spécifié de chiffres décimaux") comme défini ci-dessus.

Seulement s'il est divisé, il est converti (toujours de manière incorrecte car il utilise une échelle de 2 pour lire la constante 0.FDavant de l'étendre à 50 chiffres):

$ bc <<<'scale=50;obase=16;ibase=16; a=0.FD/1; a'
.FAE147AE147AE147AE147AE147AE147AE147AE147A

Cependant, c'est exact:

$ bc <<<'scale=50;obase=16;ibase=16; a=0.FD000000/1; a'
.FD0000000000000000000000000000000000000000

Encore une fois, la lecture de chaînes numériques (constantes) doit utiliser le nombre correct de bits.


Preuve mathématique

En deux étapes:

Une fraction binaire peut s'écrire a / 2 n

Une fraction binaire est une somme finie de puissances négatives de deux.

Par exemple:

= 0.00110101101 = 
= 0. 0     0      1     1      0      1     0      1      1     0       1

= 0 + 0 × 2 -1 + 0 × 2 -2 + 1 × 2 -3 + 1 × 2 -4 + 0 × 2 -5 + 1 × 2-6 + 0 × 2-7 + 1 × 2-8 + 1 × 2-9 + 0 × 2-10 + 1 × 2-11

= 2 -3 + 2 -4 + 2 -6 + 2 -8 + 2 -9 + 2 -11 = (avec les zéros supprimés)

Dans une fraction binaire de n bits, le dernier bit a une valeur de 2 -n , ou 1/2 n . Dans cet exemple: 2 -11 ou 1/2 11 .

= 1/2 3 + 1/2 4 + 1/2 6 + 1/2 8 + 1/2 9 + 1/2 11 = (avec inverse)

En général, le dénominateur pourrait devenir 2 n avec un exposant numérateur positif de deux. Tous les termes peuvent ensuite être combinés en une seule valeur a / 2 n . Pour cet exemple:

2 = 8 /2 11 + 2 7 /2 11 + 2 cinq / 2 11 + 2 3 /2 11 + 2 2 /2 11 + 1/2 11 = (exprimé avec deux 11 )

= (2 8 + 2 7 + 2 5 + 2 3 + 2 2 + 1) / 2 11 = (extraction du facteur commun)

= (256 + 128 + 32 + 8 + 4 + 1) / 2 11 = (converti en valeur)

= 429/2 11

Chaque fraction binaire peut être exprimée en b / 10 n

Multipliez a / 2 n par 5 n / 5 n , en obtenant (a × 5 n ) / (2 n × 5 n ) = (a × 5 n ) / 10 n = b / 10 n , où b = a × 5 n . Il a n chiffres.

Pour l'exemple, nous avons:

(429 · 5 11 ) / 10 11 = 20947265625/10 11 = 0,20947265625

Il a été démontré que chaque fraction binaire est une fraction décimale avec le même nombre de chiffres.

NotAnUnixNazi
la source