Überraschendes Ergebnis mit awk-Gleitkomma-Arithmetik

7

Ich habe versucht, awk dazu zu bringen, eine triviale Arithmetik durchzuführen, bei der einige Werte von einer Zeile zur nächsten übertragen werden.

Hier ist zum Vergleich ein minimales Beispielpaar. Das erste Beispiel ist das erwartete Verhalten, da 99,16 - 20,85 = 78,31

$ echo -e "0,99.16\n20.85,78.31" | awk -F, '{
  if (NR != 1 && (prior_tot - $1) != $2) {
    print "Arithmetic fail..." $0
  } else {
    print "OK"
  };
  prior_tot = $2
}'

Kehrt zurück

OK
OK

Das zweite Beispiel ist kein erwartetes Verhalten, da 99,15 - 20,85 = 78,30

$ echo -e "0,99.15\n20.85,78.30" | awk -F, '{
  if (NR != 1 && (prior_tot - $1) != $2) {
    print "Arithmetic fail..." $0
  } else {
    print "OK"
  };
  prior_tot = $2
}'

Kehrt zurück

OK
Arithmetic fail...20.85,78.30

Kann jemand erklären, was hier los ist?

SauceCode
quelle

Antworten:

7

Die Gleitkommazahlen 99,15 und 28,85 und 78,30 haben keine exakten IEEE 754-Binärdarstellungen. Sie können dies mit einem C-Programm sehen, das dieselbe Berechnung durchführt:

#include <stdio.h>
int
main(int ac, char **av)
{
        float a = 99.15;
        float b = 20.85;
        float c;

        printf("a = %.7f\n", a);
        printf("b = %.7f\n", b);
        c = a - b;
        printf("c = %.7f\n", c);

        return 0;
}

Ich bekomme diese Antworten von einem x86- und einem x86_64-Computer, wahrscheinlich weil beide IEEE 754- Gleitkomma-Mathematik ausführen:

a = 99,1500015 b = 20,8500004 c = 78,3000031

Folgendes passiert: Gleitkommazahlen werden mit einem Vorzeichenbit (positiv oder negativ), einer Anzahl von Bits und einem Exponenten dargestellt. Nicht jede rationale Zahl (was in diesem Zusammenhang eine "Gleitkommazahl" ist) kann genau im IEEE 754-Format dargestellt werden. So kommt die Hardware so nah wie möglich. Leider erhält die Hardware in Ihrem Testfall keine exakte Darstellung eines der drei Werte. Es wird nicht einmal, wenn Sie doublestatt verwenden float, was awkwahrscheinlich tut.

Hier ist eine weitere Erklärung des Abstands von Gleitkommazahlen mit exakten binären Darstellungen.

Sie können wahrscheinlich einige Werte finden, die Ihren Test bestehen, und andere, die dies nicht tun. Es gibt noch viel mehr, die das nicht tun.

Normalerweise lösen Menschen ein Gleitkommaproblem, indem sie Folgendes tun:

if (abs(c) <= epsilon) {
    // We'll call it equal
} else {
    // Not equal
}

Das ist viel schwieriger awk. Wenn Sie Geld mit Geldeinheiten und zwei signifikanten Ziffern der Untereinheit (z. B. Dollar und Cent) verdienen, sollten Sie nur alle Berechnungen in den Untereinheiten (Cent in den USA) durchführen. Verwenden Sie keinen Gleitkommawert für monetäre Berechnungen. Sie werden diese Entscheidung nur bereuen.

Bruce Ediger
quelle
1
awk könnte auch die Zahlen anzeigen: awk 'BEGIN{a=99.15;b=20.85;c=78.30;printf("%22.20f %22.20f %22.20f",a,b,c)}' 99.15000000000000568434 20.85000000000000142109 78.29999999999999715783Beachten Sie auch, dass die awk-Darstellung doppelt ist, Ihr c-Code einfach verwendet wird (daher der größere Fehler). Für doppelt mehr als 17 exakte Ziffern ist nicht sinnvoll.
Isaac
1
Und es gibt die Tatsache, dass IEEE784 auf gerade rundet. Einige Zahlen stimmen einfach nicht überein, egal wie lange sie gedruckt werden.
Isaac
7

Sie werden von einem Gleitkomma-Rechenproblem gebissen.

$ awk 'BEGIN { printf "%.17f\n", 99.15-20.85 }'
78.30000000000001137

http://floating-point-gui.de/ kann Ihnen möglicherweise dabei helfen, die Dinge für Sie zu klären. Es versucht zu erklären, was Gleitkomma ist und warum solche arithmetischen Fehler auftreten und wie Sie diese Art von Problemen in Ihrem System vermeiden können Programme.

godlygeek
quelle
5

Sie können solche Fehler vermeiden, indem Sie Zahlen bilden:

awk -F, '{
    if (NR != 1 && sprintf(CONVFMT,prior_tot-$1) != $2)
        {print "Arithmetic fail..." $0}
    else
        {print "OK"}
    prior_tot = $2}'
Costas
quelle
Es scheint, dass es nicht versuchtecho -e "0,99.16\n20.85,78.31\n78.31,0\n-99.15,99.15\n20.85,78.30" | awk -F, '{ if (NR != 1 && sprintf(CONVFMT,prior_tot-$1) != $2) {printf("Arithmetic fail...%s %30.30f",$0,prior_tot-$1)} else {print "OK"} prior_tot = $2}'
Isaac