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可能在一个1秒的窗口内看到A写入的时间戳“在未来”。
如今,越来越多的数据库和文件系统支持以纳秒分辨率存储时间。
注意
此问题已通过在 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 毫秒 |
| monotonic_ns() | 15 毫秒 |
| perf_counter() | 100 纳秒 |
| perf_counter_ns() | 100 纳秒 |
| process_time() | 15.6 毫秒 |
| process_time_ns() | 15.6 毫秒 |
| time() | 894.1 微秒 |
| time_ns() | 318 微秒 |
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 纳秒,这会增加精度损失。
由于此脚本运行时间少于1分钟,并且运行脚本的计算机正常运行时间少于1周,因此在此快速脚本中,time.perf_counter()、time.monotonic()、time.process_time() 及其各自的纳秒变体之间的差异不明显。如果正常运行时间达到104天或更长时间,可能会出现显著差异。
resource.getrusage() 和 times() 的分辨率大于或等于1微秒,因此不需要纳秒分辨率的变体。
注意
在内部,Python 在某些平台上将 monotonic() 和 perf_counter() 时钟设置为零,这间接减少了精度损失。
链接
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0564.rst