我以纳秒精度获取日期:
$ start=$(date '+%s.%N')
...然后打印它:
$ echo ${start}
1662664850.030126174
到目前为止,一切都很好。但是看看当我以任意大的精度 printf 时得到的结果:
1662664850.0301261739805340766906738281250000000000000000000000000
Q1. date 命令实际上是否用那么多信息填充了 start 变量,或者这些数字只是垃圾?
这是问题的第二部分。假设我想做一些时间数学。创建一个“结束”时间戳:
$ end=$(date '+%s.%N')
$ echo ${end}
1662665413.471669572
$ printf "%.55f\n" ${end}
1662665413.4716695720562711358070373535156250000000000000000000000
现在我使用 bc 得到了我期望的结果:
$ echo $(bc <<< ${end}-${start})
563.441543398
但看看我使用 python 或 perl 时得到的结果:
$ echo $(python -c "print(${end} - ${start})")
563.441543579
$ echo $(perl -e "print(${end} - ${start})")
563.441543579102
在某个时刻,这个数字会脱离轨道:
公元前563.441543第398章 蟒蛇563.441543第579章 珀尔 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 构建
答案1
正如其他人所说,浮点数的大多数十进制表示形式无法精确地以计算机用于计算的二进制格式表示。
您看到的printf %.55f
只是原始数字的二进制近似值的十进制表示形式。
您的printf
(可能的)内置命令使用转换1662665413.471669572
为long double
二进制表示形式strtold()
,然后将其传递给printf()
函数(或该系列中的其他函数,例如snprintf()
)进行格式化(%.55f
更改为%.55Lf
)
表示方式long double
因系统、C 编译器和 C 编译器以及 CPU 架构而异。对于printf
在 amd64 硬件上的 GNU/Linux 系统上使用 GNU cc 编译的 /shell,长双精度数具有足够的精度来以纳秒精度保存当前时间戳。
但是许多(如果不是大多数)工具和语言,包括大多数 awk 实现、perl、大多数 shell(对于那些支持浮点运算的 shell,而不是 bash)都使用双精度数而不是长双精度数,并且双精度数总是只有 53 位精度,因此精度不能超过 15 位小数1.
这就是为什么大多数处理高精度时间戳的东西实际上用两个整数表示它们的原因之一:秒数和微秒数(如系统struct timeval
调用返回的值gettimeofday()
)或纳秒(如系统struct timespec
调用返回的值clock_gettime()
) 。
这就是为什么 GNUdate
有%s
and 而%N
不是 的浮点变体%s
,为什么 zsh 有$epochtime
数组而不是浮点数。
要使用如此高精度的时间戳进行计算,如果您的语言/工具不使用 long double,或者在不使用 CPU 浮点运算(例如bc
)的情况下不以十进制进行任意精度计算,则可以以整数进行计算。
大多数 shell 都会使用 64 位整数进行整数运算long
,最多可以容纳 9223372036854775807 的数字。
例如,要计算两个时间戳之间的差异(将每个时间戳表示为(秒,纳秒)元组),您可以(sec2 - sec1) * 100000000 + nsec2 - nsec1
获取以纳秒为单位的差异。
例如,在 zsh 中:
zmodload zsh/datetime
start=($epochtime)
uname
end=($epochtime)
print running uname took about $((
(end[1] - start[1]) * 1_000_000_000 + end[2] - start[2] )) nanoseconds.
这里给出:
Linux
running uname took about 2621008 nanoseconds.
您可能会争辩说,在 shell 中拥有如此高的精度,运行任何非内置命令(包括date
)至少需要几千纳秒,这几乎没有意义。
像:
zmodload zsh/datetime
start=$EPOCHREALTIME
uname
end=$EPOCHREALTIME
printf 'running uname took about %.5g seconds\n' $(( end - start ))
其中$EPOCHREALTIME
具有完整的精度,因为它是通过将秒和纳秒(0 填充)整数与中间值连接起来而构建的,但是在转换为进行计算.
时会丢失一些精度,这可能已经足够好了。double
尽管在这里,就像在 ksh93 中一样,您宁愿这样做:
typeset -F SECONDS=0
uname
printf "running uname took %g seconds\n" $SECONDS
($SECONDS
,保存自 shell 启动以来的时间可以设为浮点(并重置为秒表),在这种情况下,您可以获得微秒精度)。
1 例如,printf
使用double
s 而不是long double
s 的aprintf %.16g <a-16-digit-number>
不能保证给您相同的数字(例如尝试使用9.999999999999919
和printf
ofzsh
或gawk
)perl
。
答案2
是的,使用浮点数会出现一些不精确性,这是使用有限的位或字节集来表达实数的不可避免的结果。或者也称为带有几个小数位的数字。
例如,使用您使用的号码的较短版本:
➤ printf '%.50f\n' 50.030126174
50.03012617399999999862059141264580830466002225875854
A1
Q1. date 命令实际上是否用那么多信息填充了 start 变量,或者这些数字只是垃圾?
A1.1。不,日期没有用那么多信息填充该值。
A1.2。垃圾?好吧,取决于你问谁。但是,对我来说,是的,它们几乎是垃圾。
A2
Q2。这些数字有所不同,但由于四舍五入而导致的结果与您预期的不同。是什么赋予了?
这完全是 64 位浮点数(53 位尾数)舍入的结果。
对于双精度浮点数,不超过 15 位小数位应被视为可靠。
解决方案
您已经发现 bc 运行良好,但这里还有一些替代方案:
[date]
$ date -ud "1/1/1970 + $end sec - $start sec " +'%H:%M:%S.%N'
00:09:23.441543398
[bc]
$ bc <<<"$end - $start"
563.441543398
[awk] (GNU)
$ awk -M -vPREC=200 -vend="$end" -vstart="$start" 'BEGIN{printf "%.30f\n",end - start}'
563.441543398000000000000000000000
[Perl]
$ perl -Mbignum=p,-50 -e 'print '"$end"' - '"$start"', "\n"'
563.44154339800000000000000000000000000000000000000000
[python]
$ python3 -c "from mpmath import *;mp.dps=50;print('%.30s'%(mpf('$end')-mpf('$start')));"
563.44154339800000000000000000
整数数学
但实际上start
和都不end
是浮点数。每个都是两个整数的字符串连接,中间有一个点。
我们可以将它们分开(直接在 shell 中)并使用几乎任何东西来进行数学计算,甚至是整数 shell 数学:
不可靠的数字
有些人可能会争辩说,我们可以得到给定数字的最佳表示的数学精确结果。
是的,我们可以计算很多二进制数字:
➤ bc <<<'scale=100; obase=2; 50.030126174/1'
110010.0000011110110110010110010101010000010101011001110010101110011\
00101110010000100101010100000110100011110000011110111100101011110011\
11111100000010000001101100100011111010100011011101100011001110110000\
01010011000100001110101110000010000100010000100100110100001010001001\
11011111101101001010100001100000001010000101011000101100110101001100
那是 339 个二进制数字。
但无论如何,我们必须将其放入浮点数的内存空间中(它可以有多个内存表示,但可能是长双精度数)。
我们可以选择讨论能够容纳 64 个二进制数字的浮点表示(扩展浮点,Intel FP-87 中的 80 位),这是最常见的 Linux 编译器在 x86 机器上最常用的gcc
。其他编译器可能会使用其他内容,例如 64 位双浮点数的 53 位尾数。
然后我们必须将上面的二进制数削减为这两个数字中的任何一个:
110010.0000011110110110010110010101010000010101011001110010101110
110010.0000011110110110010110010101010000010101011001110010101111
两者中,最接近原作的是最好的表示。
两个数字的精确(数学)十进制值为:
50.030126173999999998620591412645808304660022258758544921875000000
50.030126174000000002090038364599422493483871221542358398437500000
与原始数字的差异是:
00.000000000000000001379408587354191695339977741241455078125000000
00.000000000000000002090038364599422493483871221542358398437500000
因此,从数学角度来看,最好的数字是以 0 结尾的数字。
这就是为什么有些人可能会认为结果可以通过数学计算出来。
虽然这是事实,但问题就在这里。这是一个近似值,一个好的近似值,最好的近似值,但无论如何都是一个近似值。
而且,无法事先(在将实数转换为二进制之前)知道原始数和近似值之间的距离的确切大小:近似误差。
距离几乎是随机的。误差大小几乎是一个随机数。
这就是为什么我说 18 位数字(对于 64 浮点数)之后的数字是不可靠的。
对于 53 位(双精度),任何超过 15 位的数字都是不可靠的。
$ bc <<<"scale=20;l2=l(2)/l(10); b=53 ;d=((b-1)*l2);scale=0;d/1"
15
公式复制自 1967 年 DWMatula 论文,但在 C 标准中更容易找到:C11 5.2.4.2.2p11。
如果限制为 15 位,您可以看到要剪切的位置:
1662664850.030126174
1662665413.471669572
1234567890.12345
^----cut here!
这就是为什么你在 Python 和 Perl 中会遇到一些不精确的情况。
答案3
二进制浮点数学就是这样的。在大多数编程语言中,它基于 IEEE 754 标准。问题的关键在于,数字以这种格式表示为整数乘以 2 的幂;分母不是 2 的幂的有理数(例如 0.1,即 1/10)无法准确表示。
更详细的解释请查看这个答案在SO
答案4
还有一个额外的错误来源。date
返回%s
和%N
来自不同的硬件时钟,并且都是整数。纳秒值取决于高分辨率计时器是否可用:即便如此,man -s 7 time
“微秒精度是现代硬件的典型特征”。
无论这两个独立时间部分的分辨率如何,当您将它们用 a.
作为小数点粘合在一起,并将其重新解析为二进制时,拼接在一起的值只有 15 或 16 位精度。
由于纪元以来的日期现在为 10 位数字,因此时间的小数部分现在只有 5 或 6 位数字的精度。