PEP 564 – 添加具有纳秒级分辨率的新时间函数
- 作者:
- Victor Stinner <vstinner at python.org>
- 状态:
- 最终
- 类型:
- 标准轨迹
- 创建:
- 2017年10月16日
- Python 版本:
- 3.7
- 决议:
- Python-Dev 消息
摘要
向 time
模块添加六个新的“纳秒”版本的现有函数:clock_gettime_ns()
、clock_settime_ns()
、monotonic_ns()
、perf_counter_ns()
、process_time_ns()
和 time_ns()
。虽然类似于没有 _ns
后缀的现有函数,但它们提供了纳秒级分辨率:它们返回一个以 Python int
表示的纳秒数。
time.time_ns()
的分辨率在 Linux 和 Windows 上比 time.time()
的分辨率高 3 倍。
基本原理
浮点数类型限制在 104 天
台式机和笔记本电脑的时钟分辨率越来越接近纳秒级分辨率。越来越多的时钟频率为 MHz,CPU TSC 时钟的频率甚至高达 GHz。
Python time.time()
函数返回当前时间,表示为浮点数,通常为 64 位二进制浮点数(采用 IEEE 754 格式)。
问题在于,float
类型在 104 天后开始丢失纳秒。将纳秒(int
)转换为秒(float
),然后转换回纳秒(int
)以检查转换是否丢失精度
# no precision loss
>>> x = 2 ** 52 + 1; int(float(x * 1e-9) * 1e9) - x
0
# precision loss! (1 nanosecond)
>>> x = 2 ** 53 + 1; int(float(x * 1e-9) * 1e9) - x
-1
>>> print(datetime.timedelta(seconds=2 ** 53 / 1e9))
104 days, 5:59:59.254741
time.time()
返回自 UNIX 纪元(1970 年 1 月 1 日)以来的秒数。此函数自 1970 年 5 月(47 年前)以来就没有纳秒精度了
>>> import datetime
>>> unix_epoch = datetime.datetime(1970, 1, 1)
>>> print(unix_epoch + datetime.timedelta(seconds=2**53 / 1e9))
1970-04-15 05:59:59.254741
之前被拒绝的 PEP
五年前,PEP 410 提出对所有返回时间的 Python 函数进行重大而复杂的更改,以使用 decimal.Decimal
类型支持纳秒级分辨率。
该 PEP 因各种原因被拒绝
- 添加新可选参数以更改结果类型的想法被拒绝。这在 Python 中是一种不常见(且不好?)的编程实践。
- 目前尚不清楚硬件时钟是否真的具有 1 纳秒的分辨率,或者这在 Python 层面是否有意义。
decimal.Decimal
类型在 Python 中并不常见,因此需要调整代码才能处理它。
精度损失导致的问题
示例 1:在长时间运行的进程中测量时间差
服务器运行时间超过 104 天。在运行函数前后读取时钟以测量其性能,以在运行时检测性能问题。这种基准测试仅因时钟使用的浮点数类型而导致精度损失,而不是因为时钟分辨率。
在 Python 微基准测试中,通常会看到函数调用耗时少于 100 纳秒。几纳秒的差异可能变得很显著。
示例 2:比较具有不同分辨率的时间
两个程序“A”和“B”在同一系统上运行并使用系统时钟。程序 A 以纳秒级分辨率读取系统时钟并写入具有纳秒级分辨率的时间戳。程序 B 以纳秒级分辨率读取时间戳,但将其与以较差分辨率读取的系统时钟进行比较。为了简化示例,假设 B 以秒级分辨率读取时钟。在这种情况下,程序 B 可以将 A 写入的时间戳视为“将来”的时间窗口为 1 秒。
如今,越来越多的数据库和文件系统支持以纳秒级分辨率存储时间。
注意
此问题已通过向 os.stat()
结果添加 st_mtime_ns
字段以及在 os.utime()
中接受纳秒来解决文件修改时间问题。此 PEP 提出将此修复方案推广。
过去 5 年的 CPython 增强功能
由于 PEP 410 被拒绝
os.stat_result
结构为时间戳添加了 3 个新的纳秒(Pythonint
)字段:st_atime_ns
、st_ctime_ns
和st_mtime_ns
。- PEP 418 被接受,Python 3.3 获得了 3 个新的时钟:
time.monotonic()
、time.perf_counter()
和time.process_time()
。 - 处理时间的 CPython 私有“pytime” C API 现在使用新的
_PyTime_t
类型:简单的 64 位有符号整数(Cint64_t
)。_PyTime_t
的单位是实现细节,不是 API 的一部分。该单位目前为1 纳秒
。
使用纳秒作为整数的现有 Python API
os.stat_result
结构有 3 个时间戳字段,以纳秒(int
)表示:st_atime_ns
、st_ctime_ns
和 st_mtime_ns
。
os.utime()
函数的 ns
参数接受一个 (atime_ns: int, mtime_ns: int)
元组:纳秒。
更改
新函数
此 PEP 向 time
模块添加了六个新函数
time.clock_gettime_ns(clock_id)
time.clock_settime_ns(clock_id, time: int)
time.monotonic_ns()
time.perf_counter_ns()
time.process_time_ns()
time.time_ns()
这些函数类似于没有 _ns
后缀的版本,但返回以 Python int
表示的纳秒数。
例如,如果 monotonic()
值足够小以至于不会丢失精度,则 time.monotonic_ns() == int(time.monotonic() * 1e9)
。
需要这些函数是因为它们可能会返回“大”时间戳,例如使用 UNIX 纪元作为参考的 time.time()
,因此它们的 float
返回版本可能会在纳秒级分辨率下丢失精度。
未更改的函数
由于 time.clock()
函数在 Python 3.3 中已弃用,因此未添加 time.clock_ns()
。
Python 有其他返回时间的函数。对于这些其他函数,没有建议纳秒变体,因为它们的内部分辨率大于或等于 1 微秒,或者因为它们的最大值足够小以至于不会丢失精度。例如,time.clock_getres()
的最大值应为 1 秒。
未更改函数的示例
os
模块:sched_rr_get_interval()
、times()
、wait3()
和wait4()
resource
模块:getrusage()
的ru_utime
和ru_stime
字段signal
模块:getitimer()
、setitimer()
time
模块:clock_getres()
另请参阅 附录:Python 中的时钟分辨率。
如果操作系统公开了提供更高分辨率的新函数,以后可能会添加这些函数的新的纳秒返回版本。
备选方案和讨论
亚纳秒分辨率
time.time_ns()
API 从理论上讲不是面向未来的:如果时钟分辨率继续提高到低于纳秒级别,则可能需要新的 Python 函数。
实际上,1 纳秒分辨率目前足以满足所有常用操作系统函数返回的所有结构。
分辨率优于 1 纳秒的硬件时钟已经存在。例如,CPU TSC 时钟的频率是 CPU 基本频率:对于运行频率为 3 GHz 的 CPU,分辨率约为 0.3 纳秒。但是,可以访问此类硬件并真正需要亚纳秒级分辨率的用户可以根据自己的需要扩展 Python。这种罕见的用例不值得将 Python 标准库设计为支持亚纳秒级分辨率。
对于 CPython 实现,纳秒级分辨率很方便:标准且支持良好的 int64_t
类型可用于存储纳秒级精确的时间戳。它支持从 -292 年到 +292 年的时间跨度。使用 UNIX 纪元作为参考,因此它支持表示从 1677 年到 2262 年的时间
>>> 1970 - 2 ** 63 / (10 ** 9 * 3600 * 24 * 365.25)
1677.728976954687
>>> 1970 + 2 ** 63 / (10 ** 9 * 3600 * 24 * 365.25)
2262.271023045313
修改 time.time() 结果类型
有人提议修改 time.time()
以返回具有更高精度的不同数字类型。
PEP 410 提案返回 decimal.Decimal
类型,该类型已存在并支持任意精度,但该提案被否决了。除了 decimal.Decimal
之外,目前 Python 中没有其他可移植的实数类型具有更高的精度。
更改 Python 内置的 float
类型不在本 PEP 的范围内。
此外,更改现有函数以返回新的类型会带来破坏向后兼容性的风险,即使新类型的设计非常谨慎。
不同的类型
许多关于新类型以支持更大或任意精度的想法被提出:分数、使用整数的结构或 2 元组、定点数等。
另请参阅 PEP 410,其中包含之前关于其他类型的长时间讨论。
添加新类型需要付出更多努力来支持它,而不是重用现有的 int
类型。标准库、第三方代码和应用程序将需要进行修改以支持它。
Python 的 int
类型广为人知,支持良好,易于操作,并支持所有算术运算,例如 dt = t2 - t1
。
此外,获取/返回纳秒的整数数量在 Python 中并不是一个新概念,例如 os.stat_result
和 os.utime(ns=(atime_ns, mtime_ns))
就证明了这一点。
注意
如果 Python 的 float
类型变得更大(例如 decimal128 或 float128),则 time.time()
的精度也会提高。
不同的 API
建议使用 time.time(ns=False)
API 来避免添加新函数。根据参数更改结果类型在 Python 中是一种不常见(且不好?)的编程实践。
已经提出了不同的选项来允许用户选择时间分辨率。如果每个 Python 模块使用不同的分辨率,那么处理不同的分辨率可能会变得很困难,而不是仅仅使用秒(time.time()
返回 float
)和纳秒(time.time_ns()
返回 int
)。此外,如上所述,在实践中,Python 标准库不需要比 1 纳秒更好的分辨率。
一个新的模块
有人建议添加一个新的 time_ns
模块,其中包含以下函数
time_ns.clock_gettime(clock_id)
time_ns.clock_settime(clock_id, time: int)
time_ns.monotonic()
time_ns.perf_counter()
time_ns.process_time()
time_ns.time()
第一个问题是 time_ns
模块是否应该公开与 time
模块完全相同的 API(常量、函数等)。维护两种 time
模块可能会很痛苦。用户应该如何在这两个模块之间做出选择?
如果明天,os
模块需要其他纳秒变体,我们是否也需要添加一个新的 os_ns
模块?许多模块都包含与时间相关的函数:time
、os
、signal
、resource
、select
等。
另一个想法是添加一个 time.ns
子模块或嵌套命名空间以获得 time.ns.time()
语法,但它也存在相同的问题。
附录:Python 中的时钟分辨率
本附录包含在 Python 中测量的时钟分辨率,而不是操作系统宣布的分辨率或操作系统使用的内部结构的分辨率。
脚本
测量两个 time.time()
和 time.time_ns()
读取之间最小的差异(忽略零差异)的脚本示例
import math
import time
LOOPS = 10 ** 6
print("time.time_ns(): %s" % time.time_ns())
print("time.time(): %s" % time.time())
min_dt = [abs(time.time_ns() - time.time_ns())
for _ in range(LOOPS)]
min_dt = min(filter(bool, min_dt))
print("min time_ns() delta: %s ns" % min_dt)
min_dt = [abs(time.time() - time.time())
for _ in range(LOOPS)]
min_dt = min(filter(bool, min_dt))
print("min time() delta: %s ns" % math.ceil(min_dt * 1e9))
Linux
在 Fedora 26(内核 4.12)上用 Python 测量的时钟分辨率
函数 | 分辨率 |
---|---|
clock() | 1 us |
monotonic() | 81 ns |
monotonic_ns() | 84 ns |
perf_counter() | 82 ns |
perf_counter_ns() | 84 ns |
process_time() | 2 ns |
process_time_ns() | 1 ns |
resource.getrusage() | 1 us |
time() | 239 ns |
time_ns() | 84 ns |
times().elapsed | 10 ms |
times().user | 10 ms |
关于分辨率的说明
clock()
的频率是CLOCKS_PER_SECOND
,它等于 1,000,000 Hz(1 MHz):分辨率为 1 us。times()
的频率是os.sysconf("SC_CLK_TCK")
(或HZ
常量),它等于 100 Hz:分辨率为 10 ms。resource.getrusage()
、os.wait3()
和os.wait4()
使用ru_usage
结构。ru_usage.ru_utime
和ru_usage.ru_stime
字段的类型是timeval
结构,其分辨率为 1 us。
Windows
在 Windows 8.1 上用 Python 测量的时钟分辨率
函数 | 分辨率 |
---|---|
monotonic() | 15 ms |
monotonic_ns() | 15 ms |
perf_counter() | 100 ns |
perf_counter_ns() | 100 ns |
process_time() | 15.6 ms |
process_time_ns() | 15.6 ms |
time() | 894.1 us |
time_ns() | 318 us |
perf_counter()
和 perf_counter_ns()
的频率来自 QueryPerformanceFrequency()
。频率通常为 10 MHz:分辨率为 100 ns。在旧版本的 Windows 中,频率有时为 3,579,545 Hz(3.6 MHz):分辨率为 279 ns。
分析
time.time_ns()
的分辨率比 time.time()
好得多:**在 Linux 上为 84 ns(好 2.8 倍)对 239 ns,在 Windows 上为 318 us(好 2.8 倍)对 894 us**。随着时间的推移,time.time()
的分辨率只会变得更大(更差),因为每一天都会在系统时钟中增加 86,400,000,000,000 纳秒,从而增加精度损失。
在这个快速脚本中,time.perf_counter()
、time.monotonic()
、time.process_time()
及其相应的纳秒变体之间的差异不可见,因为脚本运行时间不到 1 分钟,并且用于运行脚本的计算机的运行时间小于 1 周。如果运行时间达到 104 天或更长时间,可能会看到明显的差异。
resource.getrusage()
和 times()
的分辨率大于或等于 1 微秒,因此不需要具有纳秒分辨率的变体。
注意
在内部,Python 在某些平台上将 monotonic()
和 perf_counter()
时钟从零开始,这间接减少了精度损失。
链接
版权
本文档已进入公有领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0564.rst
上次修改时间:2023-09-09 17:39:29 GMT