浮点数可以具有非常大的指数,而不会失去其显著的精度。事实证明,浮点数允许进行非常大的乘法运算而没有任何问题,如下所示:
>>> 9_000_000_000_000_000_000_000_000_000.0 * 1_000_000_000
9e+36
>>> 9_123_456_789_012_345_678_901_234_567.0 * 1_000_000_000
9.123456789012346e+36
>>> int(9_123_456_789_012_345_678_901_234_567.0 * 1_000_000_000)
9123456789012346228226434616254267392
所以基本上,浮点数会尽可能保留内部可以容纳的“有效数字”,截断剩余部分(在上面的示例中是左手操作符),然后只是调整指数。它能够粗略表示远远超过宇宙年龄的Unix纳秒时间戳。
当转换为整数时,你也可以看到浮点数保留了尽可能多的精度,并且在转换方面表现出色。所有的有效数字都在那里。输出数字末尾有很多“随机浮点舍入误差/噪音”,但这些数字并不重要。
换句话说,我对浮点数可以存储的数字大小有一个根本性的误解。它并没有固定限制。它只存储了一定数量的有效数字,然后使用指数来达到所需的比例。所以在这里使用浮点数就足够了!
答案是我可以直接进行乘法运算,完全安全。由于我的乘数是纯粹的十亿,没有任何小数部分,它只会将指数放大十亿倍,而不改变任何数字。太棒了。 :)
就像这样!
>>> int(1687976937.597064 * 1_000_000_000)
1687976937597063936
尽管我们在上面使用整数,但Python实际上会将其内部转换为浮点数(
1_000_000_000(int)-> 1e9(float)
),因为另一个操作数是浮点数。
因此,直接使用浮点数进行乘法运算实际上比先将乘数转换为浮点数再进行乘法运算要快6%。
>>> int(1687976937.597064 * 1e9)
1687976937597063936
如你所见,结果是相同的,因为两种情况都进行了
float * float
的数学运算。整数只是需要额外的转换步骤,而后一种方法避免了这一步骤。
让我们回顾一下:
1_687_976_937_597_064_018
是我之前在原问题中使用“分割”算法得到的结果。
1_687_976_937_597_063_936
是根据建议“只信任浮点数并直接进行乘法运算”得到的结果。
1_687_976_937_597_064_000
是Wolfram Alpha计算器给出的数学上正确的答案。
所以我的“分割”技术具有更小的舍入误差。我的方法更准确的原因是因为我将数字分割成了“整数”(int)和“小数/分数”(float)。这意味着我的方法将所有有效数字完全用于小数部分,因为我在小数/分数之前去除了“整数部分”。这意味着我的“小数”浮点数能够将所有有效数字用于更精确地表示小数部分。
但这些是表示为纳秒的UNIX时间戳,没有人真的关心“秒的小数精度”那么多。重要的是小数部分的前几位数字,而这些数字都是正确的。这才是最重要的。我将使用这个结果通过utimensat API在磁盘上设置时间戳,真正重要的是我能获得大致正确的秒的小数部分。:)
我使用Python中的os.utime()封装器来调用该API,它将纳秒作为有符号整数处理:“如果指定了ns,它必须是一个形如(atime_ns, mtime_ns)的2元组,其中每个成员都是表示纳秒的整数。”
我将进行直接乘法运算,然后将结果转换为整数。这样一步简单的计算就可以获得足够精确的小数部分(秒的分数),并以令人满意的方式解决了问题!
这是我将要使用的Python代码。它通过从磁盘获取该值来保留当前的“访问时间”,并将
self.unix_mtime
浮点数(UNIX时间戳,小数部分表示秒的小数)转换为有符号64位整数纳秒表示,然后将更改应用到目标文件/目录中。
file_meta = target_path.lstat()
st_mtime_ns = int(self.unix_mtime * 1e9)
os.utime(
target_path, ns=(file_meta.st_atime_ns, st_mtime_ns), follow_symlinks=False
)
如果有其他人想要这样做,请注意我正在使用lstat()
来获取符号链接的状态而不是它们的目标,并且使用follow_symlinks=False
来确保如果最终的target_path
组件是一个符号链接,则影响的是链接本身而不是目标。如果您希望影响目标而不是符号链接本身,其他人可能希望将这些调用更改为stat()
和follow_symlinks=True
。但我猜测,大多数人喜欢采用我的方法,如果target_path
指向一个符号链接,那么会影响符号链接本身。
如果您关心以最高可达精度进行此"秒浮点数到纳秒整数"转换(通过将最大浮点精度专用于所有小数位数以最小化舍入误差),则可以使用我的"分割"变体如下进行(我添加了类型提示以增加清晰度):
file_meta = target_path.lstat()
whole: int = int(self.unix_mtime)
frac: float = self.unix_mtime - whole
st_mtime_ns: int = whole * 1_000_000_000 + int(frac * 1e9)
os.utime(
target_path, ns=(file_meta.st_atime_ns, st_mtime_ns), follow_symlinks=False
)
如您所见,它使用 int * int
进行"整秒"的计算,并使用 float * float
进行"秒数的小数部分"的计算。然后将结果组合成一个整数。这在准确性和速度方面兼顾了两者的优点。
我进行了一些基准测试:
- 在 Ryzen 3900x CPU 上进行了 5000 万次迭代。
- "简化、较不准确" 版本花费了 11.728529000014532 秒。
- 更准确的版本花费了 26.941824199981056 秒。这是时间的 2.3 倍。
- 考虑到我进行了 5000 万次迭代,你可以放心地使用更准确的版本,而无需担心性能问题。所以,如果你想要更准确的时间戳,请随意使用最后一种方法。 :)
- 作为额外奖励,我对 @dawg 的答案进行了基准测试,该答案与"更准确的方法"完全相同,但是通过两次调用
math.modf()
来完成,而不是直接手动计算整数和小数部分。他们的答案是最慢的,需要 33.54755139999557 秒。我不推荐使用它。此外,他们技术背后的主要思想只是舍弃小数点后的前三位小数,这对于任何实际目的都没有影响,如果真的希望删除它们,可以通过将我的"更准确"变体的最后一行改为whole * 1_000_000_000 + (int(frac * 1e3) * 1_000_000)
来实现,而不需要慢速的 math.modf()
调用,这样可以在 27.95227960000746 秒内完成小数部分截断技术。
还有第三种方法,通过讨论的
decimal
库,它具有完美的数学精确度(不使用浮点数),但很慢,所以我没有包含它。
double
的范围大约是10^369。宇宙的年龄以纳秒计算大约是10^27。“9_007_199_254_740_992.0 是可以存储在64位浮点数中的最大数字” 这是不正确的。 - n. m. will see y'all on Redditdouble
的范围大约是10的369次方。宇宙的年龄以纳秒计约为10的27次方。 "9_007_199_254_740_992.0 是在64位浮点数中可存储的最大数字" 这是不正确的。 - n. m. could be an AI