使用 awk 浮点运算的惊人结果

Sau*_*ode 7 awk gawk

我一直试图让 awk 做一些琐碎的算术,这涉及将一些值从一行带到下一行。

这是一个最小的示例对,用于比较。第一个例子是预期行为,因为 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
}'
Run Code Online (Sandbox Code Playgroud)

退货

OK
OK
Run Code Online (Sandbox Code Playgroud)

第二个例子不是预期的行为,因为 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
}'
Run Code Online (Sandbox Code Playgroud)

退货

OK
Arithmetic fail...20.85,78.30
Run Code Online (Sandbox Code Playgroud)

任何人都可以解释这里发生了什么吗?

god*_*eek 8

您正被浮点算术问题所困扰。

$ awk 'BEGIN { printf "%.17f\n", 99.15-20.85 }'
78.30000000000001137
Run Code Online (Sandbox Code Playgroud)

http://floating-point-gui.de/可能会帮助你解决问题——它试图解释什么是浮点数,为什么会发生这样的算术错误,以及如何在你的应用程序中避免这类问题程式。


Bru*_*ger 8

浮点数 99.15、28.85 和 78.30 没有精确的 IEEE 754 二进制表示。您可以通过执行相同计算的 C 程序看到这一点:

#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;
}
Run Code Online (Sandbox Code Playgroud)

我通过 x86 和 x86_64 机器得到这些答案,可能是因为它们都进行IEEE 754浮点数学运算:

a = 99.1500015 b = 20.8500004 c = 78.3000031

这是发生的事情:浮点数用符号位(正或负)、位数和指数表示。并非每个有理数(在此上下文中就是“浮点”数)都可以以 IEEE 754 格式精确表示。因此,硬件尽可能接近。不幸的是,在您的测试案例中,硬件无法准确表示 3 个值中的任何一个。即使你使用double而不是float,它awk也不会,这可能会。

这是对具有精确二进制表示的浮点数间距的进一步解释

您可能会找到一些通过测试的值,而另一些则没有。还有很多没有。

通常人们通过做这样的事情来解决浮点问题:

if (abs(c) <= epsilon) {
    // We'll call it equal
} else {
    // Not equal
}
Run Code Online (Sandbox Code Playgroud)

awk. 如果您使用货币单位和子单位的两位有效数字(例如美元和美分)进行货币交易,您应该只在子单位(美国为美分)中执行所有计算。不要使用浮点数进行货币计算。你只会发现自己后悔那个决定。


Cos*_*tas 5

您可以通过数字格式来避免此类错误:

awk -F, '{
    if (NR != 1 && sprintf(CONVFMT,prior_tot-$1) != $2)
        {print "Arithmetic fail..." $0}
    else
        {print "OK"}
    prior_tot = $2}'
Run Code Online (Sandbox Code Playgroud)