日期命令和算术的精度

The*_*ith 4 linux date arithmetic

我以纳秒精度获取日期:

$ start=$(date '+%s.%N')
Run Code Online (Sandbox Code Playgroud)

...然后打印它:

$ echo ${start}
1662664850.030126174
Run Code Online (Sandbox Code Playgroud)

到目前为止,一切都很好。但是看看当我以任意大的精度 printf 时得到的结果:

1662664850.0301261739805340766906738281250000000000000000000000000
Run Code Online (Sandbox Code Playgroud)

Q1. date 命令实际上是否用那么多信息填充了 start 变量,或者这些数字只是垃圾?

这是问题的第二部分。假设我想做一些时间数学。创建一个“结束”时间戳:

$ end=$(date '+%s.%N')
$ echo ${end}
1662665413.471669572
$ printf "%.55f\n" ${end}
1662665413.4716695720562711358070373535156250000000000000000000000
Run Code Online (Sandbox Code Playgroud)

现在我使用 bc 得到了我期望的结果:

$ echo $(bc <<< ${end}-${start})
563.441543398
Run Code Online (Sandbox Code Playgroud)

但看看我使用 python 或 perl 时得到的结果:

$ echo $(python -c "print(${end} - ${start})")
563.441543579
$ echo $(perl -e "print(${end} - ${start})")
563.441543579102
Run Code Online (Sandbox Code Playgroud)

在某个时刻,这个数字会脱离轨道:

BC 563.441543 398   
python 563.441543 579   
perl 563.441543 579 102  

Q2。这些数字有所不同,但由于四舍五入而导致的结果与您预期的不同。是什么赋予了?

系统信息:
Linux 3.10.0-1160.71.1.el7.x86_64 #1 SMP 2022 年 6 月 15 日星期三 08:55:08 UTC 2022

命令信息:
date (GNU coreutils) 8.22
bc 1.06.95
Python 2.7.5
perl 5,版本 16,subversion 3 (v5.16.3) 为 x86_64-linux-thread-multi 构建

Sté*_*las 11

正如其他人所说,浮点数的大多数十进制表示形式无法精确地以计算机用于计算的二进制格式表示。

\n

您看到的只是printf %.55f原始数字的二进制近似值的十进制表示形式。

\n

您的printf(可能的)内置命令使用转换1662665413.471669572long double二进制表示形式strtold(),然后将其传递给printf()函数(或该系列中的其他函数,例如snprintf())进行格式化(%.55f更改为%.55Lf

\n

表示方式long double因系统、C 编译器和 C 编译器以及 CPU 架构而异。对于printf在 amd64 硬件上的 GNU/Linux 系统上使用 GNU cc 编译的 /shell,长双精度数具有足够的精度来以纳秒精度保存当前时间戳。

\n

但是许多(如果不是大多数)工具和语言,包括大多数 awk 实现、perl、大多数 shell(对于那些支持浮点算术的 shell,而不是 bash)都使用双精度数而不是长双精度数,并且双精度数总是只有 53 位精度,因此可以\' \xc2\xb9的精度远多于 15 位十进制数字

\n

这就是为什么大多数处理高精度时间戳的东西实际上用两个整数表示它们的原因之一:秒数和微秒数(如系统struct timeval调用返回的gettimeofday()值)或纳秒(如系统struct timespec调用返回的值)clock_gettime()系统调用)。

\n

这就是为什么 GNUdate%sand%N而不是 的浮点变体%s,为什么 zsh 有$epochtime数组而不是浮点数。

\n

要使用如此高精度的时间戳进行计算,如果您的语言/工具不使用 long double,或者在不使用 CPU 的浮点运算(例如 )的情况下不以十进制进行任意精度计算,您bc可以做整数。

\n

大多数 shell 都会使用 64 位long整数进行整数运算,最多可以容纳 9223372036854775807 的数字。

\n

例如,要计算两个时间戳之间的差异(将每个时间戳表示为(秒,纳秒)元组),您可以获取(sec2 - sec1) * 100000000 + nsec2 - nsec1以纳秒为单位的差异。

\n

例如,在 zsh 中:

\n
zmodload zsh/datetime\nstart=($epochtime)\nuname\nend=($epochtime)\nprint running uname took about $((\n  (end[1] - start[1]) * 1_000_000_000 + end[2] - start[2] )) nanoseconds.\n
Run Code Online (Sandbox Code Playgroud)\n

这里给出:

\n
Linux\nrunning uname took about 2621008 nanoseconds.\n
Run Code Online (Sandbox Code Playgroud)\n

您可能会争辩说,在 shell 中拥有如此高的精度,运行任何非内置命令(包括date)至少需要几千纳秒,这几乎没有意义。

\n

像:

\n
zmodload zsh/datetime\nstart=$EPOCHREALTIME\nuname\nend=$EPOCHREALTIME\nprintf \'running uname took about %.5g seconds\\n\' $(( end - start ))\n
Run Code Online (Sandbox Code Playgroud)\n

其中$EPOCHREALTIME具有完整的精度,因为它是通过将秒和纳秒(0 填充)整数与中间值连接起来而构建的,但是在转换为进行计算.时会丢失一些精度,这可能已经足够好了。double

\n

尽管在这里,就像在 ksh93 中一样,您宁愿这样做:

\n
typeset -F SECONDS=0\nuname\nprintf "running uname took %g seconds\\n" $SECONDS\n
Run Code Online (Sandbox Code Playgroud)\n

$SECONDS,保存自 shell 启动以来的时间可以设为浮点(并重置为秒表),在这种情况下,您可以获得微秒精度)。

\n
\n

例如, \xc2\xb9 ,printf使用doubles 而不是long doubles 的 a ,printf %.16g <a-16-digit-number>不能保证给您相同的数字(例如尝试使用9.999999999999919printfofzshgawkperl

\n


Rom*_*nov 5

二进制浮点数学就是这样的。在大多数编程语言中,它基于 IEEE 754 标准。问题的关键在于,数字以这种格式表示为整数乘以 2 的幂;分母不是2的幂的有理数(例如0.1,即1/10)无法精确表示

有关更详细的解释,请查看SO 中的答案


Qua*_*tal 5

是的,使用浮点数会出现一些不精确性,这是使用有限的位或字节集来表达实数的不可避免的结果。或者也称为带有几个小数位的数字。

\n

例如,使用您使用的号码的较短版本:

\n
 \xe2\x9e\xa4 printf '%.50f\\n' 50.030126174\n\n50.03012617399999999862059141264580830466002225875854\n
Run Code Online (Sandbox Code Playgroud)\n

A1

\n
\n

Q1. date 命令实际上是否用那么多信息填充了 start 变量,或者这些数字只是垃圾?

\n
\n

A1.1。不,日期没有用那么多信息填充该值。

\n

A1.2。垃圾?好吧,取决于你问谁。但是,对我来说,是的,它们几乎是垃圾。

\n

A2

\n

Q2。这些数字有所不同,但由于四舍五入而导致的结果与您预期的不同。是什么赋予了?

\n

这完全是 64 位浮点数(53 位尾数)舍入的结果。

\n

对于双精度浮点数,不超过 15 位小数位应被视为可靠。

\n

解决方案

\n

您已经发现 bc 工作正常,但这里有一些替代方案:

\n
[date]\n$ date -ud "1/1/1970 + $end sec - $start sec " +'%H:%M:%S.%N'\n00:09:23.441543398\n\n[bc]\n$ bc <<<"$end - $start"\n563.441543398\n\n[awk] (GNU) \n$ awk -M -vPREC=200 -vend="$end" -vstart="$start" 'BEGIN{printf "%.30f\\n",end - start}'\n563.441543398000000000000000000000\n\n[Perl]\n$ perl -Mbignum=p,-50 -e 'print '"$end"' - '"$start"', "\\n"'\n563.44154339800000000000000000000000000000000000000000\n\n[python]\n$ python3 -c "from mpmath import *;mp.dps=50;print('%.30s'%(mpf('$end')-mpf('$start')));"\n563.44154339800000000000000000\n
Run Code Online (Sandbox Code Playgroud)\n

整数数学

\n

但实际上 和start都不end是浮点数。每个都是两个整数的字符串连接,中间有一个点。

\n

我们可以将它们分开(直接在 shell 中)并使用几乎任何东西来进行数学计算,甚至是整数 shell 数学:

\n

不可靠的数字

\n

有些人可能会争辩说,我们可以得到给定数字的最佳表示的数学精确结果。

\n

是的,我们可以计算很多二进制数字:

\n
 \xe2\x9e\xa4 bc <<<'scale=100; obase=2; 50.030126174/1'\n110010.0000011110110110010110010101010000010101011001110010101110011\\\n00101110010000100101010100000110100011110000011110111100101011110011\\\n11111100000010000001101100100011111010100011011101100011001110110000\\\n01010011000100001110101110000010000100010000100100110100001010001001\\\n11011111101101001010100001100000001010000101011000101100110101001100\n
Run Code Online (Sandbox Code Playgroud)\n

那是 339 个二进制数字。

\n

但无论如何,我们必须将其放入浮点数的内存空间中(它可以有多个内存表示,但可能是长双精度数)。

\n

我们可以选择讨论能够容纳 64 个二进制数字的浮点表示(扩展浮点,Intel FP-87 中的 80 位),这是最常见的 Linux 编译器在 x86 机器上最常用的gcc。其他编译器可能会使用其他内容,例如 64 位双浮点数的 53 位尾数。

\n

然后我们必须将上面的二进制数削减为这两个数字中的任何一个:

\n
110010.0000011110110110010110010101010000010101011001110010101110\n110010.0000011110110110010110010101010000010101011001110010101111\n
Run Code Online (Sandbox Code Playgroud)\n

两者中,最接近原著的就是最好的代表。

\n

两个数字的精确(数学)十进制值为:

\n
50.030126173999999998620591412645808304660022258758544921875000000\n50.030126174000000002090038364599422493483871221542358398437500000 \n
Run Code Online (Sandbox Code Playgroud)\n

与原始数字的差异是:

\n
00.000000000000000001379408587354191695339977741241455078125000000\n00.000000000000000002090038364599422493483871221542358398437500000\n
Run Code Online (Sandbox Code Playgroud)\n

因此,从数学角度来看,最好的数字是以 0 结尾的数字。

\n

这就是为什么有些人可能会认为结果可以通过数学计算出来。

\n

虽然这是事实,但问题就在这里。这是一个近似值,一个好的近似值,最好的近似值,但无论如何都是一个近似值。

\n

而且,无法事先(在将实数转换为二进制之前)知道原始数和近似值之间的距离的确切大小:近似误差。

\n

距离几乎是随机的。误差大小几乎是一个随机数。

\n

这就是为什么我说 18 位数字(对于 64 浮点数)之后的数字是不可靠的。

\n

对于 53 位(双精度),任何超过 15 位的数字都是不可靠的。

\n
$ bc <<<"scale=20;l2=l(2)/l(10); b=53 ;d=((b-1)*l2);scale=0;d/1"\n15\n
Run Code Online (Sandbox Code Playgroud)\n

公式复制自 1967 年 DWMatula 论文,但在 C 标准中更容易找到:C11 5.2.4.2.2p11。

\n

如果限制为 15 位,您可以看到要剪切的位置:

\n
1662664850.030126174\n1662665413.471669572\n1234567890.12345\n                ^----cut here!\n
Run Code Online (Sandbox Code Playgroud)\n

这就是为什么你在 Python 和 Perl 中会遇到一些不精确的情况。

\n