Perdere precisione con dc


12

Voglio usare dcper gestire alcuni numeri di base 16 con punti esadecimali ma sto incontrando problemi di precisione. Ad esempio, sotto sto moltiplicando F423F.FDper 100, entrambi esadecimali. La risposta attesa è F423FFDinvece dare F423FFA.E1, vicino ma non abbastanza accurata anche dopo l'arrotondamento.

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

Ho letto che dcera un calcolatore di precisione illimitato, e questo non è un gran numero in alcun modo. C'è qualcosa che sto facendo di sbagliato?

Grazie per le tue risposte Dati i problemi con dc, ho morso il proiettile e ho scritto il mio parser per numeri reali in altre basi. Se qualcuno è interessato al codice, posso pubblicarlo qui.

Risposte:


6

Si noti che solo la stampa del numero originale mostra che è arrotondato:

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

Puoi aggirarlo aggiungendo molti zero finali per una maggiore precisione:

$ dc <<<'16 d i o F423F.FD000000 100 * p'
F423FFD.0000000

Grazie. Penso che finirà per prendere più codice per massaggiare i numeri dcda usare e poi scrivere direttamente un parser! (L'input può avere o meno un decimale e può trovarsi in altre basi, quindi la quantità di riempimento varia.)
Yimin Rong

2
Lo segnerò come risposta accettata. Le persone responsabili del mantenimento hanno dcrisposto: Per gestire correttamente le cifre frazionarie non decimali richiederebbe un modello completamente diverso rispetto al modello in scala decimale utilizzato da dc e bc (come dettato da POSIX per bc e dalla tradizione storica per entrambi). , quindi tecnicamente potrebbe essere risolto dc, ma probabilmente si sarebbe rotto bc, così classificato come WONTFIX.
Yimin Rong,

8

Espresso come decimale (usando dcper convertire), questo corrisponde a 999999,98 (arrotondato per difetto) × 256, cioè 255999994,88, che è F423FFA.E1 in esadecimale.

Quindi la differenza deriva dal dccomportamento di arrotondamento: invece di calcolare 256 × (999999 + 253 ÷ 256), che darebbe 255999997, arrotonderà per difetto 253 ÷ 256 e moltiplica il risultato.

dcè un calcolatore di precisione arbitrario , il che significa che può calcolare qualsiasi precisione tu voglia, ma devi dirgli di cosa si tratta. Per impostazione predefinita, la precisione è 0, il che significa che la divisione produce solo valori interi e la moltiplicazione utilizza il numero di cifre nell'input. Per impostare la precisione, utilizzare k(e tenere presente che la precisione è sempre espressa in cifre decimali, indipendentemente dal radix di input o output):

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

(La precisione di 8 cifre sarebbe sufficiente poiché questo è ciò che è necessario rappresentare 1 ÷ 256 in decimale.)


1
Sembrerebbe un risultato del tutto inaspettato per un calcolatore di "precisione arbitraria"?
Yimin Rong,

3
Perde ancora la precisione quando kè impostato: 10 k 16 d i o F423F.FD pF423F.FA, quindi dovrei ingrandire tutti i numeri prima di usarli dc. Fondamentalmente equivale a pre-analizzarli comunque.
Yimin Rong,

2
@Yimin sì, sfortunatamente ridimensiona il dcsuo input usando solo il numero di cifre, il che mi sembra un bug (dal momento che il numero di cifre viene calcolato usando la radix di input, ma applicato al valore decimale).
Stephen Kitt,

1
@dhag è ciò che POSIX specifica (per bc, su cui dcsi basa): "I calcoli interni devono essere condotti come in decimali, indipendentemente dalle basi di input e output, al numero specificato di cifre decimali."
Stephen Kitt,

1
È davvero un problema di come viene analizzata una costante. Prova 20 k 16 d i o 0.3 1 / p (che stampa .1999999999999999999). Capire che l'operazione è semplicemente dividendo 0.2per 1(che in teoria non dovrebbe modificare il valore). Mentre 20 k 16 d i o 0.3000 1 / p(correttamente) stampa .30000000000000000. (Cont.)
NotAnUnixNazi

1

Il problema

Il problema è il modo in cui dc (e bc) comprendono le costanti numeriche.
Ad esempio, il valore (in esadecimale) 0.3(diviso per 1) viene trasformato in un valore vicino a0.2

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

In effetti, anche la costante normale 0.3viene cambiata:

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

Sembra che sia in un modo strano, ma non lo è (più tardi).
L'aggiunta di più zeri rende l'approccio alla risposta il valore corretto:

$ 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

L'ultimo valore è esatto e rimarrà esatto indipendentemente da come possano essere aggiunti più zeri.

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

Il problema è presente anche in 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

Una cifra per bit?

Il fatto molto intuitivo per i numeri in virgola mobile è che il numero di cifre richieste (dopo il punto) è uguale al numero di bit binari (anche dopo il punto). Un numero binario 0,101 è esattamente uguale a 0,625 in decimale. Il numero binario 0,0001110001 è (esattamente) uguale a 0.1103515625(dieci cifre decimali)

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

Inoltre, per un numero in virgola mobile come 2 ^ (- 10), che in binario ha solo un bit (impostato):

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

Ha lo stesso numero di cifre binarie .0000000001(10) delle cifre decimali .0009765625(10). Questo potrebbe non essere il caso in altre basi, ma la base 10 è la rappresentazione interna dei numeri sia in cc che in cc e quindi è l'unica base di cui dobbiamo davvero preoccuparci.

La prova matematica è alla fine di questa risposta.

scala in bc

Il numero di cifre dopo il punto può essere contato con il scale()modulo di funzione incorporato bc:

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

Come mostrato, 2 cifre non sono sufficienti per rappresentare la costante 0.FD.

Inoltre, contare il numero di caratteri utilizzati dopo il punto è un modo molto errato di riportare (e utilizzare) la scala del numero. La scala di un numero (in qualsiasi base) dovrebbe calcolare il numero di bit necessari.

Cifre binarie in un float esadecimale.

Come è noto, ogni cifra esadecimale utilizza 4 bit. Pertanto, ogni cifra esadecimale dopo il punto decimale richiede 4 cifre binarie, che a causa del fatto (dispari?) Sopra richiedono anche 4 cifre decimali.

Pertanto, un numero simile 0.FDrichiederà 8 cifre decimali per essere rappresentato correttamente:

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

Aggiungi zeri

La matematica è semplice (per numeri esadecimali):

  • Contare il numero di cifre esadecimali ( h) dopo il punto.
  • Moltiplicare hper 4.
  • Aggiungi h×4 - h = h × (4-1) = h × 3 = 3×hzeri.

Nel codice shell (per 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

Che stamperà (correttamente sia in cc che in cc):

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

Internamente, bc (o dc) potrebbe far corrispondere il numero di cifre richieste al numero calcolato sopra ( 3*h) per convertire i float esadecimali nella rappresentazione decimale interna. O qualche altra funzione per altre basi (supponendo che il numero di cifre sia finito rispetto alla base 10 (interna di bc e dc) in tale altra base). Come 2 i (2,4,8,16, ...) e 5,10.

posix

La specifica posix afferma che (per bc, su cui si basa dc):

I calcoli interni devono essere condotti come in decimali, indipendentemente dalle basi di input e output, al numero specificato di cifre decimali.

Ma "... il numero specificato di cifre decimali." potrebbe essere inteso come "... il numero necessario di cifre decimali per rappresentare la costante numerica" ​​(come descritto sopra) senza influire sui "calcoli interni decimali"

Perché:

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

bc non utilizza realmente 50 ("il numero specificato di cifre decimali") come impostato sopra.

Solo se diviso viene convertito (ancora erroneamente poiché utilizza una scala 2 per leggere la costante 0.FDprima di espanderla a 50 cifre):

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

Tuttavia, questo è esatto:

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

Ancora una volta, la lettura di stringhe numeriche (costanti) dovrebbe utilizzare il numero corretto di bit.


Prova matematica

In due passaggi:

Una frazione binaria può essere scritta come / 2 n

Una frazione binaria è una somma finita di poteri negativi di due.

Per esempio:

= 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 = (con zeri rimossi)

In una frazione binaria di n bit, l'ultimo bit ha un valore di 2 -n o 1/2 n . In questo esempio: 2 -11 o 1/2 11 .

= 1/2 3 + 1/2 4 + 1/2 6 + 1/2 8 + 1/2 9 + 1/2 11 = (con inverso)

In generale, il denominatore potrebbe diventare 2 n con un esponente numeratore positivo di due. Tutti i termini possono quindi essere combinati in un singolo valore a / 2 n . Per questo esempio:

= 2 8 /2 11 + 2 7 /2 11 + 2 5 /2 11 + 2 3 /2 11 + 2 2 /2 11 + 1/2 11 = (espressa con 2 11 )

= (2 8 + 2 7 + 2 5 + 2 3 + 2 2 + 1) / 2 11 = (estrazione del fattore comune)

= (256 + 128 + 32 + 8 + 4 + 1) / 2 11 = (convertito in valore)

= 429/2 11

Ogni frazione binaria può essere espressa come b / 10 n

Moltiplicare a / 2 n per 5 n / 5 n , ottenendo (a × 5 n ) / (2 n × 5 n ) = (a × 5 n ) / 10 n = b / 10 n , dove b = a × 5 n . Ha n cifre.

Per l'esempio, abbiamo:

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

È stato dimostrato che ogni frazione binaria è una frazione decimale con lo stesso numero di cifre.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.