Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 418 – 添加单调时间、性能计数器和进程时间函数

作者:
Cameron Simpson <cs at cskk.id.au>, Jim J. Jewett <jimjjewett at gmail.com>, Stephen J. Turnbull <stephen at xemacs.org>, Victor Stinner <vstinner at python.org>
状态:
最终
类型:
标准跟踪
创建:
2012-03-26
Python 版本:
3.3

目录

摘要

本 PEP 提案向 Python 3.3 添加 time.get_clock_info(name)time.monotonic()time.perf_counter()time.process_time() 函数。

基本原理

如果程序使用系统时间来安排事件或实现超时,当系统时间被手动更改或被 NTP 自动调整时,它可能无法在正确的时间运行事件,或者过早或过晚停止超时。应使用单调时钟以避免受到系统时间更新的影响:time.monotonic()

要测量函数的性能,可以使用 time.clock(),但它在 Windows 和 Unix 上的行为非常不同。在 Windows 上,time.clock() 包括睡眠期间经过的时间,而在 Unix 上则不包括。time.clock() 在 Windows 上的分辨率非常高,但在 Unix 上则非常低。应改用新的 time.perf_counter() 函数,以始终获得具有可移植行为的最精确性能计数器(例如:包括睡眠期间花费的时间)。

到目前为止,Python 尚未直接提供可移植函数来测量 CPU 时间。可以使用 time.clock() 在 Unix 上使用,但它的分辨率很低。还可以使用 resource.getrusage()os.times() 在 Unix 上使用,但它们需要计算内核空间和用户空间中花费的总时间。新的 time.process_time() 函数充当一个可移植计数器,它始终测量 CPU 时间(不包括睡眠期间经过的时间),并具有最佳的分辨率。

每个操作系统都以不同的方式实现时钟和性能计数器,了解所使用的具体函数以及时钟的一些属性(如分辨率)非常有用。新的 time.get_clock_info() 函数可以访问有关每个 Python 时间函数的所有可用信息。

新函数

  • time.monotonic():超时和调度,不受系统时钟更新的影响
  • time.perf_counter():基准测试,用于短时间段的最精确时钟
  • time.process_time():分析,进程的 CPU 时间

新函数的用户

  • time.monotonic():concurrent.futures、multiprocessing、queue、subprocess、telnet 和 threading 模块,用于实现超时
  • time.perf_counter():trace 和 timeit 模块,pybench 程序
  • time.process_time():profile 模块
  • time.get_clock_info():pybench 程序,用于显示有关计时器的信息,如分辨率

time.clock() 函数已弃用,因为它不可移植:它根据操作系统的不同而表现出不同的行为。应改用 time.perf_counter()time.process_time(),具体取决于您的需求。time.clock() 被标记为已弃用,但目前没有计划删除。

限制

  • 系统挂起后时钟的行为在新的函数文档中没有定义。该行为取决于操作系统:请参阅下面的 单调时钟 部分。一些最新的操作系统提供两个时钟,一个包括系统挂起期间经过的时间,一个不包括此时间。大多数操作系统只提供一种时钟。
  • time.monotonic() 和 time.perf_counter() 可能会或可能不会被调整。例如,CLOCK_MONOTONIC 在 Linux 上会被调整,而 GetTickCount() 在 Windows 上不会被调整。可以使用 time.get_clock_info('monotonic')['adjustable'] 来检查单调时钟是否可调整。
  • 本 PEP 并没有提出 time.thread_time() 函数,因为它不需要 Python 标准库,也不是一个常见的请求功能。此函数仅在 Windows 和 Linux 上可用。在 Linux 上,可以使用 time.clock_gettime(CLOCK_THREAD_CPUTIME_ID)。在 Windows 上,可以使用 ctypes 或其他模块来调用 GetThreadTimes() 函数。

Python 函数

新函数

time.get_clock_info(name)

获取有关指定时钟的信息。支持的时钟名称

  • "clock"time.clock()
  • "monotonic"time.monotonic()
  • "perf_counter"time.perf_counter()
  • "process_time"time.process_time()
  • "time"time.time()

返回一个 time.clock_info 对象,该对象具有以下属性

  • implementation (str):底层操作系统函数的名称。示例:"QueryPerformanceCounter()""clock_gettime(CLOCK_REALTIME)"
  • monotonic (bool):如果时钟无法倒退,则为 True。
  • adjustable (bool):如果时钟可以自动更改(例如,由 NTP 守护程序)或由系统管理员手动更改,则为 True,否则为 False
  • resolution (float):时钟的分辨率(以秒为单位)。

time.monotonic()

单调时钟,即无法倒退。它不受系统时钟更新的影响。返回的值的参考点是未定义的,因此只有连续调用结果之间的差才是有效的,并且是一个秒数。

在 Vista 之前的 Windows 版本中,time.monotonic() 检测到 GetTickCount() 整数溢出(32 位,在 49.7 天后回滚)。它在每次检测到溢出时将内部纪元(参考时间)增加 232。纪元存储在进程本地状态中,因此在运行超过 49 天的两个 Python 进程中,time.monotonic() 的值可能不同。在更新版本的 Windows 和其他操作系统上,time.monotonic() 是系统范围的。

可用性:Windows、Mac OS X、Linux、FreeBSD、OpenBSD、Solaris。在 GNU/Hurd 上不可用。

伪代码 [2]

if os.name == 'nt':
    # GetTickCount64() requires Windows Vista, Server 2008 or later
    if hasattr(_time, 'GetTickCount64'):
        def monotonic():
            return _time.GetTickCount64() * 1e-3
    else:
        def monotonic():
            ticks = _time.GetTickCount()
            if ticks < monotonic.last:
                # Integer overflow detected
                monotonic.delta += 2**32
            monotonic.last = ticks
            return (ticks + monotonic.delta) * 1e-3
        monotonic.last = 0
        monotonic.delta = 0

elif sys.platform == 'darwin':
    def monotonic():
        if monotonic.factor is None:
            factor = _time.mach_timebase_info()
            monotonic.factor = timebase[0] / timebase[1] * 1e-9
        return _time.mach_absolute_time() * monotonic.factor
    monotonic.factor = None

elif hasattr(time, "clock_gettime") and hasattr(time, "CLOCK_HIGHRES"):
    def monotonic():
        return time.clock_gettime(time.CLOCK_HIGHRES)

elif hasattr(time, "clock_gettime") and hasattr(time, "CLOCK_MONOTONIC"):
    def monotonic():
        return time.clock_gettime(time.CLOCK_MONOTONIC)

在 Windows 上,即使 QueryPerformanceCounter() 的分辨率高于 GetTickCount(),也不会使用它。它不可靠,并且存在太多问题。

time.perf_counter()

具有最高可用分辨率的性能计数器,用于测量短时间段。它包括睡眠期间经过的时间,并且是系统范围的。返回的值的参考点是未定义的,因此只有连续调用结果之间的差才是有效的,并且是一个秒数。

它在所有平台上都可用。

伪代码

if os.name == 'nt':
    def _win_perf_counter():
        if _win_perf_counter.frequency is None:
            _win_perf_counter.frequency = _time.QueryPerformanceFrequency()
        return _time.QueryPerformanceCounter() / _win_perf_counter.frequency
    _win_perf_counter.frequency = None

def perf_counter():
    if perf_counter.use_performance_counter:
        try:
            return _win_perf_counter()
        except OSError:
            # QueryPerformanceFrequency() fails if the installed
            # hardware does not support a high-resolution performance
            # counter
            perf_counter.use_performance_counter = False
    if perf_counter.use_monotonic:
        # The monotonic clock is preferred over the system time
        try:
            return time.monotonic()
        except OSError:
            perf_counter.use_monotonic = False
    return time.time()
perf_counter.use_performance_counter = (os.name == 'nt')
perf_counter.use_monotonic = hasattr(time, 'monotonic')

time.process_time()

当前进程的系统和用户 CPU 时间之和。它不包括睡眠期间经过的时间。它根据定义是进程范围的。返回的值的参考点是未定义的,因此只有连续调用结果之间的差才是有效的。

它在所有平台上都可用。

伪代码 [2]

if os.name == 'nt':
    def process_time():
        handle = _time.GetCurrentProcess()
        process_times = _time.GetProcessTimes(handle)
        return (process_times['UserTime'] + process_times['KernelTime']) * 1e-7
else:
    try:
        import resource
    except ImportError:
        has_resource = False
    else:
        has_resource = True

    def process_time():
        if process_time.clock_id is not None:
            try:
                return time.clock_gettime(process_time.clock_id)
            except OSError:
                process_time.clock_id = None
        if process_time.use_getrusage:
            try:
                usage = resource.getrusage(resource.RUSAGE_SELF)
                return usage[0] + usage[1]
            except OSError:
                process_time.use_getrusage = False
        if process_time.use_times:
            try:
                times = _time.times()
                cpu_time = times.tms_utime + times.tms_stime
                return cpu_time / process_time.ticks_per_seconds
            except OSError:
                process_time.use_getrusage = False
        return _time.clock()
    if (hasattr(time, 'clock_gettime')
        and hasattr(time, 'CLOCK_PROF')):
        process_time.clock_id = time.CLOCK_PROF
    elif (hasattr(time, 'clock_gettime')
          and hasattr(time, 'CLOCK_PROCESS_CPUTIME_ID')):
        process_time.clock_id = time.CLOCK_PROCESS_CPUTIME_ID
    else:
        process_time.clock_id = None
    process_time.use_getrusage = has_resource
    process_time.use_times = hasattr(_time, 'times')
    if process_time.use_times:
        # sysconf("SC_CLK_TCK"), or the HZ constant, or 60
        process_time.ticks_per_seconds = _times.ticks_per_seconds

现有函数

time.time()

通常是民用时间的系统时间。它根据定义是系统范围的。它可以由系统管理员手动设置,也可以由 NTP 守护程序自动设置。

它在所有平台上都可用,并且不会失败。

伪代码 [2]

if os.name == "nt":
    def time():
        return _time.GetSystemTimeAsFileTime()
else:
    def time():
        if hasattr(time, "clock_gettime"):
            try:
                return time.clock_gettime(time.CLOCK_REALTIME)
            except OSError:
                # CLOCK_REALTIME is not supported (unlikely)
                pass
        if hasattr(_time, "gettimeofday"):
            try:
                return _time.gettimeofday()
            except OSError:
                # gettimeofday() should not fail
                pass
        if hasattr(_time, "ftime"):
            return _time.ftime()
        else:
            return _time.time()

time.sleep()

将执行挂起指定秒数。实际挂起时间可能小于请求的时间,因为任何捕获的信号都将终止 time.sleep(),然后执行该信号的捕获例程。此外,由于系统中其他活动的调度,挂起时间可能会比请求的时间长任意量。

伪代码 [2]

try:
    import select
except ImportError:
    has_select = False
else:
    has_select = hasattr(select, "select")

if has_select:
    def sleep(seconds):
        return select.select([], [], [], seconds)

elif hasattr(_time, "delay"):
    def sleep(seconds):
        milliseconds = int(seconds * 1000)
        _time.delay(milliseconds)

elif os.name == "nt":
    def sleep(seconds):
        milliseconds = int(seconds * 1000)
        win32api.ResetEvent(hInterruptEvent);
        win32api.WaitForSingleObject(sleep.sigint_event, milliseconds)

    sleep.sigint_event = win32api.CreateEvent(NULL, TRUE, FALSE, FALSE)
    # SetEvent(sleep.sigint_event) will be called by the signal handler of SIGINT

elif os.name == "os2":
    def sleep(seconds):
        milliseconds = int(seconds * 1000)
        DosSleep(milliseconds)

else:
    def sleep(seconds):
        seconds = int(seconds)
        _time.sleep(seconds)

已弃用函数

time.clock()

在 Unix 上,返回当前处理器时间,以秒为单位的浮点数表示。它根据定义是进程范围的。分辨率,事实上,“处理器时间”的定义本身取决于同名 C 函数,但无论如何,这是用于对 Python 或计时算法进行基准测试的函数。

在 Windows 上,此函数基于 Win32 函数 QueryPerformanceCounter(),返回自第一次调用此函数以来的经过的时钟秒数,以浮点数表示。分辨率通常优于 1 微秒。它是系统范围的。

伪代码 [2]

if os.name == 'nt':
    def clock():
        try:
            return _win_perf_counter()
        except OSError:
            # QueryPerformanceFrequency() fails if the installed
            # hardware does not support a high-resolution performance
            # counter
            pass
        return _time.clock()
else:
    clock = _time.clock

替代方案:API 设计

time.monotonic() 的其他名称

  • time.counter()
  • time.metronomic()
  • time.seconds()
  • time.steady(): “steady” 的含义很模糊:不同的人理解不同。例如,在 Linux 上,CLOCK_MONOTONIC 会被调整。如果我们使用真实时间作为参考时钟,我们就可以说 CLOCK_MONOTONIC 是稳定的。但是 CLOCK_MONOTONIC 在系统挂起时会暂停,而真实时间包括挂起期间的任何时间。
  • time.timeout_clock()
  • time.wallclock(): time.monotonic() 不是系统时间,也称为“挂钟”,而是一个具有未指定起始点的单调时钟。

对于 time.monotonic() 的旧版本,也有人建议使用名称“time.try_monotonic()”,该版本在没有单调时钟可用时会回退到系统时间。

time.perf_counter() 的其他名称

  • time.high_precision()
  • time.highres()
  • time.hires()
  • time.performance_counter()
  • time.timer()

仅公开操作系统时钟

为了避免定义高层时钟(这是一项困难的任务),一个更简单的方法是只公开操作系统时钟。例如,time.clock_gettime() 和相关的时钟标识符已添加到 Python 3.3 中。

time.monotonic(): 回退到系统时间

如果没有单调时钟可用,time.monotonic() 会回退到系统时间。

问题

  • 很难在文档中正确定义这样一个函数:它是单调的吗?它是稳定的吗?它是否被调整了?
  • 一些用户希望决定在没有单调时钟可用时该怎么办:使用另一个时钟、显示错误或执行其他操作。

人们提出了不同的 API 来定义这样的函数。

使用标志的一个函数:time.monotonic(fallback=True)

  • time.monotonic(fallback=True) 在没有单调时钟可用或单调时钟失败时回退到系统时间。
  • time.monotonic(fallback=False) 在单调时钟失败时引发 OSError,如果系统没有提供单调时钟,则引发 NotImplementedError。

在调用者中作为常量传递的关键字参数通常是糟糕的 API。

在 Python 中,为函数引发 NotImplementedError 是不常见的,应避免。

一个 time.monotonic() 函数,无标志

time.monotonic() 返回 (time: float, is_monotonic: bool)。

另一种方法是使用函数属性:time.monotonic.is_monotonic。在第一次调用 time.monotonic() 之前,属性值将为 None。

从一组约束中选择时钟

PEP 中提出的方法提供了几个新的时钟,但它们保证的范围故意很宽松,以便在不同的平台上提供有用的时钟。这在本质上将策略嵌入到调用中,因此调用者必须选择一个策略。

“选择一个时钟”方法建议添加一个额外的 API,让调用者可以通过使大多数平台时钟可用并让调用者从中选择来实现自己的策略(如有必要)。PEP 中建议的时钟仍然有望在常见简单的用例中可用。

为此,需要两种机制:时钟枚举和关于时钟的元数据,以使用户能够评估其适用性。

主要接口是一个函数,使简单的选择变得容易:调用者可以使用 time.get_clock(*flags),并使用一些标志组合。这至少包括

  • time.MONOTONIC: 时钟不能后退
  • time.STEADY: 时钟速率稳定
  • time.ADJUSTED: 时钟可能会被调整,例如通过 NTP
  • time.HIGHRES: 具有最高分辨率的时钟

它返回一个时钟对象,该对象具有一个 .now() 方法,用于返回当前时间。时钟对象用描述时钟功能集的元数据进行注释;它的 .flags 字段将至少包含所有请求的标志。

如果找不到匹配的时钟,time.get_clock() 将返回 None,因此可以使用或运算符链接调用。简单策略决策示例

T = get_clock(MONOTONIC) or get_clock(STEADY) or get_clock()
t = T.now()

可用的时钟至少始终包括 time.time() 的包装器,因此可以使用不带任何标志的最终调用来始终获得一个工作时钟。

系统时钟标志示例

  • QueryPerformanceCounter: MONOTONIC | HIGHRES
  • GetTickCount: MONOTONIC | STEADY
  • CLOCK_MONOTONIC: MONOTONIC | STEADY(或仅在 Linux 上为 MONOTONIC)
  • CLOCK_MONOTONIC_RAW: MONOTONIC | STEADY
  • gettimeofday():(无标志)

时钟对象包含其他元数据,包括时钟标志(具有上述标志之外的附加功能标志)、底层操作系统工具的名称以及时钟精度。

time.get_clock() 仍然选择一个单一时钟;还需要一个枚举机制。最明显的方法是提供 time.get_clocks(),其签名与 time.get_clock() 相同,但返回匹配请求标志的所有时钟的序列。因此,请求不带任何标志将枚举所有可用的时钟,使调用者能够根据其元数据在其中进行任意选择。

示例部分实现:clockutils.py.

解决操作系统错误?

Python 应该通过计算时钟值和前一个值的最大值来确保单调时钟是真正单调的吗?

由于使用静态变量缓存上次返回的值相对简单,因此使用它来确保返回的值确实是单调的可能很有趣。

  • 虚拟机提供的时钟不太可靠。
  • QueryPerformanceCounter() 存在已知的错误(只有一个尚未修复)

Python 只能解决特定已知的操作系统错误:KB274323 包含一个代码示例,用于解决此错误(使用 GetTickCount() 来检测 QueryPerformanceCounter() 跳跃)。

解决非单调性的问题

  • 如果时钟意外提前一个小时,然后又回退,那么你将无法在整个小时内使用有用的时钟
  • 缓存不是在进程之间共享的,因此不同的进程不会看到相同的时钟值

术语表

精度:
给定仪器的测量值与真实值的偏差量。另请参阅 精度和准确度。时钟的误差可能是由于缺乏精度、漂移或时钟初始设置不正确(例如,线程计时天生不准确,因为重置计数器的完美同步非常困难)。
调整:
将时钟重置为正确的时间。这可以通过 <Step> 或 <Slewing> 来完成。
民用时间:
一天中的时间;外部于系统。上午 10:45:13 是民用时间;45 秒不是。由现有函数 time.localtime()time.gmtime() 提供。此 PEP 未更改。
时钟:
用于测量时间的仪器。不同的时钟具有不同的特性;例如,具有纳秒 <精度> 的时钟可能会在几分钟后开始 <漂移>,而精度较低的时钟可以保持准确数天。此 PEP 主要关注使用秒为单位的时钟。
计数器:
每次发生特定事件时都会递增的时钟。计数器是严格单调的,但不是单调时钟。它可以用来生成唯一(且有序)的时间戳,但这些时间戳无法映射到 <民用时间>;滴答声创建很可能很突发,同一毫秒内会发生几次递增,然后连续几天都不会递增。
CPU 时间:
衡量在特定任务上花费了多少 CPU 资源。CPU 秒通常是标准化的(因此可以在相同的实际秒内发生可变数量)。CPU 秒在分析时可能很重要,但它们不会直接映射到用户响应时间,也不可以直接与(真实)秒进行比较。
漂移:
相对于系统外部定义的“真实”时间的累计误差。漂移可能是由于不精确或时钟时间前进的平均速率与真实时间前进的速率之间的差异造成的。
纪元:
时钟的参考点。对于提供 <民用时间> 的时钟,这通常是 1970 年 1 月 1 日的午夜,因为那天(和一年)开始了。对于 <clock_monotonic> 时钟,纪元可能未定义(表示为 None)。
延迟:
延迟。时钟调用返回时,<真实时间> 已经过去了,可能比时钟的精度还要长。
单调:
人们期望单调时钟在实践中具有的特性。最多在一个方向上移动;对于时钟来说,这个方向是前进。<时钟> 还应该 <稳定>,并且应该可以转换为秒为单位的时间。权衡通常包括没有定义的 <纪元> 或映射到 <民用时间>。
精度:
单一仪器对同一物理值的测量值之间的偏差量。时钟的不精确性可能是由于时钟时间相对于真实时间的推进速率波动造成的,包括时钟通过倾斜调整造成的波动。
进程时间:
进程开始后经过的时间。它通常以 <CPU 时间> 而不是 <真实时间> 来衡量,并且通常在进程挂起时不会推进。
真实时间:
现实世界中的时间。这与 <民用时间> 不同,因为它不会被 <调整>,但它们应该同步前进。它与“实时 [操作系统]”的“实时”无关。有时称为“挂钟时间”以避免这种歧义;不幸的是,这带来了不同的歧义。
分辨率:
给定仪器测量到的两个物理值之间的最小差异,导致不同的测量结果。
倾斜:
对时钟速度的微小改变,通常是为了校正相对于外部权威的 <漂移>。
稳定性:
准确性的持久性。衡量预期 <漂移>。
稳定:
具有高 <稳定性> 以及相对较高 <准确度> 和 <精度> 的时钟。在实践中,它通常用来表示 <clock_monotonic> 时钟,但更强调后续滴答声之间持续时间的一致性。
步进:
对表示时间的瞬时更改。而不是加快或减慢时钟 (<倾斜>),而是永久地添加一个偏移量。
系统时间:
操作系统表示的时间。
线程时间:
线程开始后经过的时间。它通常以 <CPU 时间> 而不是 <真实时间> 来衡量,并且通常在线程处于空闲状态时不会推进。
挂钟:
墙上的时钟显示的内容。这通常用作 <真实时间> 的同义词;不幸的是,挂钟时间本身也存在歧义。

硬件时钟

硬件时钟列表

  • HPET:高精度事件计时器 (HPET) 芯片包含一个 64 位向上计数器(主计数器),其计数频率至少为 10 MHz,以及一组最多 256 个比较器(至少 3 个)。每个 HPET 可以有最多 32 个计时器。HPET 每天的漂移量约为 3 秒。
  • TSC(时间戳计数器):历史上,TSC 随着每个内部处理器时钟周期而增加,但现在速率通常是恒定的(即使处理器改变频率),通常等于最大处理器频率。多个核心具有不同的 TSC 值。系统的休眠状态将重置 TSC 值。RDTSC 指令可用于读取此计数器。CPU 频率缩放以节省电量。
  • ACPI 电源管理计时器:ACPI 24 位计时器,频率为 3.5 MHz(3,579,545 Hz)。
  • Cyclone:Cyclone 计时器在 IBM 扩展 X 架构 (EXA) 芯片组上使用 32 位计数器,其中包括使用 IBM“Summit”系列芯片组的计算机(例如:x440)。它在 IA32 和 IA64 架构中可用。
  • PIT(可编程中断计时器):英特尔 8253/8254 芯片组,频率可配置,范围为 18.2 Hz - 1.2 MHz。它使用 16 位计数器。
  • RTC(实时时钟)。大多数 RTC 使用频率为 32,768 Hz 的晶体振荡器。

Linux clocksource

Linux 内核中有 4 种时间实现:UTIME(1996)、计时轮(1997)、HRT(2001)和 hrtimers(2007)。后者是 George Anzinger 于 2001 年启动的“高分辨率计时器”项目的成果,Thomas Gleixner 和 Douglas Niehaus 也做出了贡献。hrtimers 实现合并到 2007 年发布的 Linux 2.6.21 中。

hrtimers 支持多种时钟源。它为每个来源设置一个优先级来决定使用哪个来源。Linux 支持以下时钟源

  • tsc
  • hpet
  • pit
  • pmtmr:ACPI 电源管理计时器
  • cyclone

并非所有硬件架构都支持高分辨率计时器。它们至少在 x86/x86_64、ARM 和 PowerPC 上提供。

无论底层时钟源如何,CLOCK_REALTIMECLOCK_MONOTONICclock_getres() 都返回 1 纳秒。请阅读 Thomas Gleixner(2012 年 2 月 9 日)的 Re: clock_getres() and real resolution 以获取解释。

/sys/devices/system/clocksource/clocksource0 目录包含两个有用的文件

  • available_clocksource:可用时钟源列表
  • current_clocksource:当前使用的时钟源。可以通过将时钟源的名称写入此文件来更改当前时钟源。

/proc/timer_list 包含所有硬件计时器的列表。

另请阅读 time(7) 手册页:“时间和计时器的概述”。

FreeBSD timecounter

kern.timecounter.choice 列出了可用的硬件时钟及其优先级。sysctl 程序可用于更改 timecounter。示例

# dmesg | grep Timecounter
Timecounter "i8254" frequency 1193182 Hz quality 0
Timecounter "ACPI-safe" frequency 3579545 Hz quality 850
Timecounter "HPET" frequency 100000000 Hz quality 900
Timecounter "TSC" frequency 3411154800 Hz quality 800
Timecounters tick every 10.000 msec
# sysctl kern.timecounter.choice
kern.timecounter.choice: TSC(800) HPET(900) ACPI-safe(850) i8254(0) dummy(-1000000)
# sysctl kern.timecounter.hardware="ACPI-fast"
kern.timecounter.hardware: HPET -> ACPI-fast

可用的时钟

  • “TSC”:处理器的 Time Stamp Counter
  • “HPET”:高精度事件计时器
  • “ACPI-fast”:ACPI 电源管理计时器(快速模式)
  • “ACPI-safe”:ACPI 电源管理计时器(安全模式)
  • “i8254”:具有英特尔 8254 芯片组的 PIT

提交 222222(2011 年 5 月)将 ACPI-fast timecounter 质量降低到 900,并将 HPET timecounter 质量提高到 950:“现代平台上的 HPET 通常比 ACPI 计时器具有更好的分辨率和更低的延迟”。

阅读 Poul-Henning Kamp(2002 年)为 FreeBSD 项目撰写的 Timecounters: Efficient and precise timekeeping in SMP kernels

性能

读取硬件时钟是有成本的。下表比较了在具有英特尔酷睿 i7-2600(3.40 GHz,8 核)的 Linux 3.3 上不同硬件时钟的性能。使用 bench_time.c 程序填充这些表。

函数 TSC ACPI PM HPET
time() 2 ns 2 ns 2 ns
CLOCK_REALTIME_COARSE 10 ns 10 ns 10 ns
CLOCK_MONOTONIC_COARSE 12 ns 13 ns 12 ns
CLOCK_THREAD_CPUTIME_ID 134 ns 135 ns 135 ns
CLOCK_PROCESS_CPUTIME_ID 127 ns 129 ns 129 ns
clock() 146 ns 146 ns 143 ns
gettimeofday() 23 ns 726 ns 637 ns
CLOCK_MONOTONIC_RAW 31 ns 716 ns 607 ns
CLOCK_REALTIME 27 ns 707 ns 629 ns
CLOCK_MONOTONIC 27 ns 723 ns 635 ns

FreeBSD 8.0 在具有硬件虚拟化的 kvm 中

函数 TSC ACPI-Safe HPET i8254
time() 191 ns 188 ns 189 ns 188 ns
CLOCK_SECOND 187 ns 184 ns 187 ns 183 ns
CLOCK_REALTIME_FAST 189 ns 180 ns 187 ns 190 ns
CLOCK_UPTIME_FAST 191 ns 185 ns 186 ns 196 ns
CLOCK_MONOTONIC_FAST 188 ns 187 ns 188 ns 189 ns
CLOCK_THREAD_CPUTIME_ID 208 ns 206 ns 207 ns 220 ns
CLOCK_VIRTUAL 280 ns 279 ns 283 ns 296 ns
CLOCK_PROF 289 ns 280 ns 282 ns 286 ns
clock() 342 ns 340 ns 337 ns 344 ns
CLOCK_UPTIME_PRECISE 197 ns 10380 ns 4402 ns 4097 ns
CLOCK_REALTIME 196 ns 10376 ns 4337 ns 4054 ns
CLOCK_MONOTONIC_PRECISE 198 ns 10493 ns 4413 ns 3958 ns
CLOCK_UPTIME 197 ns 10523 ns 4458 ns 4058 ns
gettimeofday() 202 ns 10524 ns 4186 ns 3962 ns
CLOCK_REALTIME_PRECISE 197 ns 10599 ns 4394 ns 4060 ns
CLOCK_MONOTONIC 201 ns 10766 ns 4498 ns 3943 ns

每个函数调用了 100,000 次,并使用 CLOCK_MONOTONIC 获取之前和之后的时间。基准测试运行了 5 次,保留最小时间。

NTP 调整

NTP 有不同的方法来调整时钟

  • “斜坡”:更改时钟频率以略微快或慢(使用 adjtime() 完成)。由于斜率限制为每秒 0.5 毫秒,因此每秒调整都需要 2000 秒的摊销间隔。因此,几秒钟的调整可能需要数小时或数天才能摊销。
  • “步进”:在单个离散步骤中跳跃大量(使用 settimeofday() 完成)

默认情况下,如果偏移量小于 128 毫秒,则时间会斜坡,否则会步进。

如果希望测量“真实”时间(而不是像 CPU 周期这样的类似时间的对象),则通常需要斜坡(即我们应该使用 CLOCK_MONOTONIC,而不是 CLOCK_MONOTONIC_RAW)。这是因为与您来自 NTP 连接另一端的时钟可能更擅长计时:毕竟,那 35,000 美元的铯计时设备可能做得比您 PC 上 3 美元的石英晶体要好得多。

NTP 守护程序文档 中获取更多详细信息。

操作系统时间函数

单调时钟

名称 C 分辨率 已调整 包括休眠 包括挂起
gethrtime() 1 ns
CLOCK_HIGHRES 1 ns
CLOCK_MONOTONIC 1 ns 在 Linux 上斜坡
CLOCK_MONOTONIC_COARSE 1 ns 在 Linux 上斜坡
CLOCK_MONOTONIC_RAW 1 ns
CLOCK_BOOTTIME 1 ns ?
CLOCK_UPTIME 1 ns ?
mach_absolute_time() 1 ns
QueryPerformanceCounter() - ?
GetTickCount[64]() 1 ms
timeGetTime() 1 ms ?

“C 分辨率”列是底层 C 结构的分辨率。

x86_64 上时钟分辨率的示例

名称 操作系统 操作系统分辨率 Python 分辨率
QueryPerformanceCounter Windows 七 10 ns 10 ns
CLOCK_HIGHRES SunOS 5.11 2 ns 265 ns
CLOCK_MONOTONIC Linux 3.0 1 ns 322 ns
CLOCK_MONOTONIC_RAW Linux 3.3 1 ns 628 ns
CLOCK_BOOTTIME Linux 3.3 1 ns 628 ns
mach_absolute_time() Mac OS 10.6 1 ns 3 µs
CLOCK_MONOTONIC FreeBSD 8.2 11 ns 5 µs
CLOCK_MONOTONIC OpenBSD 5.0 10 ms 5 µs
CLOCK_UPTIME FreeBSD 8.2 11 ns 6 µs
CLOCK_MONOTONIC_COARSE Linux 3.3 1 ms 1 ms
CLOCK_MONOTONIC_COARSE Linux 3.0 4 ms 4 ms
GetTickCount64() Windows 七 16 ms 15 ms

“操作系统分辨率”是操作系统宣布的分辨率。“Python 分辨率”是使用 clock_resolution.py 程序在 Python 中计算的两次调用时间函数之间的最小差值。

mach_absolute_time

Mac OS X 提供了一个单调时钟:mach_absolute_time()。它基于系统启动后的绝对经过时间。它不会被调整,也不能被设置。

mach_timebase_info() 提供了一个分数来将时钟值转换为纳秒数。另请参阅 技术问答 QA1398

mach_absolute_time() 在 PowerPC CPU 上休眠期间停止,但在 Intel CPU 上不会停止:Different behaviour of mach_absolute_time() on i386/ppc

CLOCK_MONOTONIC, CLOCK_MONOTONIC_RAW, CLOCK_BOOTTIME

CLOCK_MONOTONIC 和 CLOCK_MONOTONIC_RAW 代表自某个未指定起点以来的单调时间。它们不能被设置。可以使用 clock_getres() 读取分辨率。

文档:参考您操作系统的帮助手册。示例

CLOCK_MONOTONIC 至少在以下操作系统上可用

  • DragonFly BSD、FreeBSD >= 5.0、OpenBSD、NetBSD
  • Linux
  • Solaris

以下操作系统不支持 CLOCK_MONOTONIC

在 Linux 上,NTP 可能会调整 CLOCK_MONOTONIC 速率(斜坡),但它不能向后跳跃。

CLOCK_MONOTONIC_RAW 是 Linux 特定的。它类似于 CLOCK_MONOTONIC,但提供对不受 NTP 调整影响的原始基于硬件的时间的访问。CLOCK_MONOTONIC_RAW 需要 Linux 2.6.28 或更高版本。

Linux 2.6.39 和 glibc 2.14 引入了一个新的时钟:CLOCK_BOOTTIME。CLOCK_BOOTTIME 与 CLOCK_MONOTONIC 相同,只是它还包括在挂起期间花费的任何时间。另请阅读 Waking systems from suspend(2011 年 3 月)。

CLOCK_MONOTONIC 在机器挂起时停止。

Linux 自 Linux 2.6.32 以来还提供了 CLOCK_MONOTONIC_COARSE。它类似于 CLOCK_MONOTONIC,精度较低,但速度更快。

clock_gettime() 如果系统不支持指定的时钟,即使标准 C 库支持它,也会失败。例如,CLOCK_MONOTONIC_RAW 需要内核版本 2.6.28 或更高版本。

Windows: QueryPerformanceCounter

高分辨率性能计数器。它是单调的。可以使用 QueryPerformanceFrequency() 读取计数器的频率。分辨率为 1 / QueryPerformanceFrequency()。

它具有更高的分辨率,但与 GetTickCount() 和 timeGetTime() 时钟相比,长期精度较低。例如,它会相对于低精度时钟发生漂移。

文档

QueryPerformanceCounter() 使用的硬件时钟

  • Windows XP:英特尔处理器的 RDTSC 指令,时钟频率是处理器的频率(在 200 MHz 和 3 GHz 之间,如今通常大于 1 GHz)。
  • Windows 2000:ACPI 电源管理计时器,频率 = 3,549,545 Hz。可以通过 boot.ini 中的“/usepmtimer”标志强制执行。

QueryPerformanceFrequency() 应该只调用一次:系统运行期间频率不会改变。如果已安装的硬件不支持高分辨率性能计数器,它将失败。

QueryPerformanceCounter() 不能被调整:SetSystemTimeAdjustment() 只会调整系统时间。

错误

  • 性能计数器值可能会因硬件错误而意外跳跃,请参见 KB274323
  • 在 VirtualBox 上,QueryPerformanceCounter() 不会在低位部分溢出时每次递增高位部分,请参见 Monotonic timers(2009 年)。
  • VirtualBox 在其虚拟化的 HPET 设备中存在错误:QueryPerformanceCounter() 会向前跳跃大约 42 秒(问题 #8707)。
  • Windows XP 存在一个错误(参见 KB896256):在多处理器计算机上,QueryPerformanceCounter() 返回每个处理器的不同值。该错误在 Windows XP SP2 中已修复。
  • 具有可变频率的处理器的问题:频率会根据工作负载更改以减少内存消耗。
  • Chromium 在 Athlon X2 CPU(型号 15)上不使用 QueryPerformanceCounter(),因为“QueryPerformanceCounter 不可靠”(参见 Chromium 源代码中的 base/time_win.cc)

Windows: GetTickCount(), GetTickCount64()

GetTickCount() 和 GetTickCount64() 是单调的,不会失败,也不会被 SetSystemTimeAdjustment() 调整。MSDN 文档:GetTickCount()GetTickCount64()。分辨率可以使用 GetSystemTimeAdjustment() 读取。

通过 GetTickCount() 或 GetTickCount64() 获取的经过时间包括系统在睡眠或休眠状态下花费的时间。

GetTickCount64() 已添加到 Windows Vista 和 Windows Server 2008。

可以使用 未公开的 NtSetTimerResolution() 函数 来提高精度。有一些应用程序使用此未公开的函数,例如:计时器分辨率

WaitForSingleObject() 使用与 GetTickCount() 相同的计时器,精度相同。

Windows: timeGetTime

timeGetTime 函数以毫秒为单位检索系统时间。系统时间是 Windows 启动后经过的时间。阅读 timeGetTime() 文档

timeGetTime() 的返回类型是 32 位无符号整数。与 GetTickCount() 一样,timeGetTime() 在 2^32 毫秒(49.7 天)后会回滚。

通过 timeGetTime() 获取的经过时间包括系统在睡眠状态下花费的时间。

timeGetTime 函数的默认精度可以是 5 毫秒或更多,具体取决于机器。

timeBeginPeriod() 可用于将 timeGetTime() 的精度提高到 1 毫秒,但这会对功耗产生负面影响。调用 timeBeginPeriod() 还会影响其他一些计时调用的粒度,例如 CreateWaitableTimer()、WaitForSingleObject() 和 Sleep()。

注意

timeGetTime() 和 timeBeginPeriod() 是 Windows 多媒体库的一部分,因此需要将程序链接到 winmm 或动态加载库。

Solaris: CLOCK_HIGHRES

Solaris OS 具有一个 CLOCK_HIGHRES 计时器,该计时器尝试使用最佳硬件源,并且可能提供接近纳秒级的分辨率。CLOCK_HIGHRES 是不可调整的高分辨率时钟。对于使用 clockid_t 值为 CLOCK_HIGHRES 创建的计时器,系统将尝试使用最佳硬件源。

CLOCK_HIGHRES 的分辨率可以使用 clock_getres() 读取。

Solaris: gethrtime

gethrtime() 函数返回当前高分辨率实时。时间表示为从过去某个任意时间起的纳秒数;它与当天的时间没有任何关联,因此不会通过 adjtime() 或 settimeofday() 受到重置或漂移的影响。hires 计时器非常适合性能测量任务,在这种任务中需要廉价且准确的间隔计时。

gethrtime() 的线性在挂起-恢复周期中不会保留(错误 4272663)。

阅读 Solaris 11 的 gethrtime() 手册页

在 Solaris 上,gethrtime() 与 clock_gettime(CLOCK_MONOTONIC) 相同。

系统时间

名称 C 分辨率 包括休眠 包括挂起
CLOCK_REALTIME 1 ns
CLOCK_REALTIME_COARSE 1 ns
GetSystemTimeAsFileTime 100 纳秒
gettimeofday() 1 微秒
ftime() 1 ms
time() 1 秒

“C 分辨率”列是底层 C 结构的分辨率。

x86_64 上时钟分辨率的示例

名称 操作系统 操作系统分辨率 Python 分辨率
CLOCK_REALTIME SunOS 5.11 10 ms 238 纳秒
CLOCK_REALTIME Linux 3.0 1 ns 238 纳秒
gettimeofday() Mac OS 10.6 1 微秒 4 微秒
CLOCK_REALTIME FreeBSD 8.2 11 ns 6 µs
CLOCK_REALTIME OpenBSD 5.0 10 ms 5 µs
CLOCK_REALTIME_COARSE Linux 3.3 1 ms 1 ms
CLOCK_REALTIME_COARSE Linux 3.0 4 ms 4 ms
GetSystemTimeAsFileTime() Windows 七 16 ms 1 ms
ftime() Windows 七 - 1 ms

“操作系统分辨率”是操作系统宣布的分辨率。“Python 分辨率”是使用 clock_resolution.py 程序在 Python 中计算的两次调用时间函数之间的最小差值。

Windows: GetSystemTimeAsFileTime

可以使用 GetSystemTimeAsFileTime()、ftime() 和 time() 读取系统时间。系统时间的分辨率可以使用 GetSystemTimeAdjustment() 读取。

阅读 GetSystemTimeAsFileTime() 文档

可以使用 SetSystemTime() 设置系统时间。

UNIX 上的系统时间

gettimeofday()、ftime()、time() 和 clock_gettime(CLOCK_REALTIME) 返回系统时间。CLOCK_REALTIME 的分辨率可以使用 clock_getres() 读取。

可以使用 settimeofday() 或 clock_settime(CLOCK_REALTIME) 设置系统时间。

自 Linux 2.6.32 起,Linux 还提供 CLOCK_REALTIME_COARSE。它类似于 CLOCK_REALTIME,精度较低,但速度更快。

Alexander Shishkin 提出了一种用于 Linux 的 API,以便在系统时钟更改时收到通知:timerfd:添加 TFD_NOTIFY_CLOCK_SET 来监视时钟更改(API 的第 4 个版本,2011 年 3 月)。该 API 尚未被接受,但 CLOCK_BOOTTIME 提供了类似的功能。

进程时间

无法设置进程时间。它不是单调的:当进程处于空闲状态时,时钟会停止。

名称 C 分辨率 包括休眠 包括挂起
GetProcessTimes() 100 纳秒
CLOCK_PROCESS_CPUTIME_ID 1 ns
getrusage(RUSAGE_SELF) 1 微秒
times() -
clock() - Windows 上是,其他平台上不是

“C 分辨率”列是底层 C 结构的分辨率。

x86_64 上时钟分辨率的示例

名称 操作系统 操作系统分辨率 Python 分辨率
CLOCK_PROCESS_CPUTIME_ID Linux 3.3 1 ns 1 ns
CLOCK_PROF FreeBSD 8.2 10 ms 1 微秒
getrusage(RUSAGE_SELF) FreeBSD 8.2 - 1 微秒
getrusage(RUSAGE_SELF) SunOS 5.11 - 1 微秒
CLOCK_PROCESS_CPUTIME_ID Linux 3.0 1 ns 1 微秒
getrusage(RUSAGE_SELF) Mac OS 10.6 - 5 µs
clock() Mac OS 10.6 1 微秒 5 µs
CLOCK_PROF OpenBSD 5.0 - 5 µs
getrusage(RUSAGE_SELF) Linux 3.0 - 4 ms
getrusage(RUSAGE_SELF) OpenBSD 5.0 - 8 毫秒
clock() FreeBSD 8.2 8 毫秒 8 毫秒
clock() Linux 3.0 1 微秒 10 ms
times() Linux 3.0 10 ms 10 ms
clock() OpenBSD 5.0 10 ms 10 ms
times() OpenBSD 5.0 10 ms 10 ms
times() Mac OS 10.6 10 ms 10 ms
clock() SunOS 5.11 1 微秒 10 ms
times() SunOS 5.11 1 微秒 10 ms
GetProcessTimes() Windows 七 16 ms 16 ms
clock() Windows 七 1 ms 1 ms

“操作系统分辨率”是操作系统宣布的分辨率。“Python 分辨率”是使用 clock_resolution.py 程序在 Python 中计算的两次调用时间函数之间的最小差值。

函数

  • Windows:GetProcessTimes()。分辨率可以使用 GetSystemTimeAdjustment() 读取。
  • clock_gettime(CLOCK_PROCESS_CPUTIME_ID):来自 CPU 的高分辨率每进程计时器。分辨率可以使用 clock_getres() 读取。
  • clock()。分辨率为 1 / CLOCKS_PER_SEC。
    • Windows:自进程启动以来的经过挂钟时间(以秒为单位的经过时间乘以 CLOCKS_PER_SEC)。包括睡眠期间的经过时间。它可能失败。
    • UNIX:返回程序使用的处理器时间的近似值。
  • getrusage(RUSAGE_SELF) 返回当前进程的资源使用情况结构。ru_utime 是用户 CPU 时间,ru_stime 是系统 CPU 时间。
  • times():进程时间结构。分辨率为 1 / ticks_per_seconds,其中 ticks_per_seconds 是 sysconf(_SC_CLK_TCK) 或 HZ 常量。

Python 源代码包含一个便携式库来获取进程时间(CPU 时间):Tools/pybench/systimes.py

另请参见 QueryProcessCycleTime() 函数(所有线程的循环时间的总和)和 clock_getcpuclockid()

线程时间

无法设置线程时间。它不是单调的:当线程处于空闲状态时,时钟会停止。

名称 C 分辨率 包括休眠 包括挂起
CLOCK_THREAD_CPUTIME_ID 1 ns 纪元更改
GetThreadTimes() 100 纳秒 ?

“C 分辨率”列是底层 C 结构的分辨率。

x86_64 上时钟分辨率的示例

名称 操作系统 操作系统分辨率 Python 分辨率
CLOCK_THREAD_CPUTIME_ID FreeBSD 8.2 1 微秒 1 微秒
CLOCK_THREAD_CPUTIME_ID Linux 3.3 1 ns 649 纳秒
GetThreadTimes() Windows 七 16 ms 16 ms

“操作系统分辨率”是操作系统宣布的分辨率。“Python 分辨率”是使用 clock_resolution.py 程序在 Python 中计算的两次调用时间函数之间的最小差值。

函数

  • Windows:GetThreadTimes()。分辨率可以使用 GetSystemTimeAdjustment() 读取。
  • clock_gettime(CLOCK_THREAD_CPUTIME_ID):线程特定的 CPU 时间时钟。它使用 CPU 周期的数量,而不是秒的数量。分辨率可以使用 clock_getres() 读取。

另请参见 QueryThreadCycleTime() 函数(指定线程的循环时间)和 pthread_getcpuclockid()。

Windows: QueryUnbiasedInterruptTime

从有偏差的中断时间和当前睡眠偏差量获取当前无偏差的中断时间。此时间不受电源管理睡眠转换的影响。

QueryUnbiasedInterruptTime 函数获取的经过时间仅包括系统在工作状态下花费的时间。QueryUnbiasedInterruptTime() 不是单调的。

QueryUnbiasedInterruptTime() 是在 Windows 7 中引入的。

另请参见 QueryIdleProcessorCycleTime() 函数(每个处理器的空闲线程的循环时间)

睡眠

将进程的执行挂起给定秒数。睡眠不受系统时间更新的影响。睡眠在系统挂起期间暂停。例如,如果进程睡眠 60 秒,系统在睡眠中间挂起 30 秒,则睡眠持续时间在实际时间中为 90 秒。

睡眠可以被信号中断:该函数将使用 EINTR 失败。

名称 C 分辨率
nanosleep() 1 ns
clock_nanosleep() 1 ns
usleep() 1 微秒
delay() 1 微秒
sleep() 1 秒

其他函数

名称 C 分辨率
sigtimedwait() 1 ns
pthread_cond_timedwait() 1 ns
sem_timedwait() 1 ns
select() 1 微秒
epoll() 1 ms
poll() 1 ms
WaitForSingleObject() 1 ms

“C 分辨率”列是底层 C 结构的分辨率。

函数

clock_nanosleep

clock_nanosleep(clock_id, flags, nanoseconds, remaining): Linux 的 clock_nanosleep() 手册页

如果 flags 是 TIMER_ABSTIME,则请求将被解释为由时钟 clock_id 测量的绝对时间。如果请求小于或等于时钟的当前值,则 clock_nanosleep() 会立即返回,而不会挂起调用线程。

POSIX.1 指定,通过 clock_settime(2) 更改 CLOCK_REALTIME 时钟的值,对阻塞在相对 clock_nanosleep() 上的线程没有影响。

select()

select(nfds, readfds, writefds, exceptfs, timeout)。

自 Linux 2.6.28 起,select() 使用高分辨率计时器来处理超时。进程具有一个“松弛”属性来配置超时的精度,默认松弛为 50 微秒。在 Linux 2.6.28 之前,select() 的超时由主计时子系统以 jiffy 级分辨率处理。另请阅读 高(但不太高)分辨率超时计时器松弛

其他函数

  • poll()、epoll()
  • sigtimedwait()。POSIX:“如果支持单调时钟选项,则应使用 CLOCK_MONOTONIC 时钟来测量由 timeout 参数指定的时间间隔。”
  • pthread_cond_timedwait()、pthread_condattr_setclock()。“时钟属性的默认值应引用系统时间。”
  • sem_timedwait():“如果支持计时器选项,则超时应基于 CLOCK_REALTIME 时钟。如果不支持计时器选项,则超时应基于 time() 函数返回的系统时间。超时的精度应为其基于的时钟的精度。”
  • WaitForSingleObject():使用与 GetTickCount() 相同的计时器,精度相同。

系统待机

ACPI 电源状态“S3”是系统待机模式,也称为“挂起到内存”。内存保持供电状态。

在 Windows 上,WM_POWERBROADCAST 消息被发送到 Windows 应用程序,以通知它们电源管理事件(例如:所有者状态已更改)。

对于 Mac OS X,请阅读 注册和取消注册睡眠和唤醒通知(技术问答 QA1340)。

脚注

接受

PEP 于 2012 年 4 月 28 日由 Guido van Rossum 接受 [1]。PEP 实现已提交到存储库。

参考文献


来源: https://github.com/python/peps/blob/main/peps/pep-0418.rst

上次修改时间: 2023-09-09 17:39:29 GMT