PEP 564: add process_time()

Explain why other functions don't need a nanosecond variant.
This commit is contained in:
Victor Stinner 2017-10-18 01:00:37 +02:00
parent 9d8fd95001
commit fcbf6126d3
1 changed files with 113 additions and 51 deletions

View File

@ -13,14 +13,15 @@ Python-Version: 3.7
Abstract
========
Add five new functions to the ``time`` module: ``time_ns()``,
``perf_counter_ns()``, ``monotonic_ns()``, ``clock_gettime_ns()`` and
``clock_settime_ns()``. They are similar to the function without the
``_ns`` suffix, but have nanosecond resolution: use a number of
nanoseconds as a Python int.
Add six new "nanosecond" variant of existing functions to the ``time``
module: ``clock_gettime_ns()``, ``clock_settime_ns()``,
``monotonic_ns()``, ``perf_counter_ns()``, ``process_time_ns()`` and
``time_ns()``. Similar to the existing functions without the ``_ns``
suffix, they have nanosecond resolution: use a number of nanoseconds as
a Python int.
The best ``time.time_ns()`` resolution measured in Python is 3 times
better then ``time.time()`` resolution on Linux and Windows.
The ``time.time_ns()`` resolution measured in Python is 3 times better
than the ``time.time()`` resolution on Linux and Windows.
Rationale
@ -83,13 +84,14 @@ The PEP was rejected for different reasons:
Issues caused by precision loss
-------------------------------
Example 1: measure time delta
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Example 1: measure time delta in long running process
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
A server is running for longer than 104 days. A clock is read before
and after running a function to measure its performance. This benchmark
lose precision only because the float type used by clocks, not because
of the clock resolution.
A server is running for longer than 104 days. A clock is read before and
after running a function to measure its performance to detect
performance issues at runtime. Such benchmark only lose precision
because of the float type used by clocks, not because of the clock
resolution.
On Python microbenchmarks, it is common to see function calls taking
less than 100 ns. A difference of a single nanosecond becomes
@ -98,8 +100,8 @@ significant.
Example 2: compare time with different resolution
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Two programs "A" and "B" are runing on the same system, so use the system
block. The program A reads the system clock with nanosecond resolution
Two programs "A" and "B" are runing on the same system and use the system
clock. The program A reads the system clock with nanosecond resolution
and writes the timestamp with nanosecond resolution. The program B reads
the timestamp with nanosecond resolution, but compares it to the system
clock read with a worse resolution. To simplify the example, let's say
@ -152,12 +154,13 @@ Changes
New functions
-------------
This PEP adds five new functions to the ``time`` module:
This PEP adds six new functions to the ``time`` module:
* ``time.clock_gettime_ns(clock_id)``
* ``time.clock_settime_ns(clock_id, time: int)``
* ``time.perf_counter_ns()``
* ``time.monotonic_ns()``
* ``time.perf_counter_ns()``
* ``time.process_time_ns()``
* ``time.time_ns()``
These functions are similar to the version without the ``_ns`` suffix,
@ -166,16 +169,22 @@ but use nanoseconds as Python ``int``.
For example, ``time.monotonic_ns() == int(time.monotonic() * 1e9)`` if
``monotonic()`` value is small enough to not lose precision.
These functions are needed because they handle big timestamps, like
time.time() which uses the UNIX epoch as reference, and so their version
without the ``_ns`` suffix are likely to lose precision at the
nanosecond resolution.
Unchanged functions
-------------------
This PEP only proposed to add new functions getting or setting clocks
with nanosecond resolution. Clocks are likely to lose precision,
especially when their reference is the UNIX epoch.
Since the ``time.clock()`` function was deprecated in Python 3.3, no
``time.clock_ns()`` is added.
Python has other functions handling time (get time, timeout, etc.), but
no nanosecond variant is proposed for them since they are less likely to
lose precision.
Python has other functions handling time. No nanosecond variant was
proposed because their internal resolution is greater or equal to 1 us,
or because their maximum value is a small enough to lose precision. For
example, the maximum value of ``clock_getres()`` is likely to be 1
second.
Example of unchanged functions:
@ -189,11 +198,10 @@ Example of unchanged functions:
* ``time`` module: ``clock_getres()``
Since the ``time.clock()`` function was deprecated in Python 3.3, no
``time.clock_ns()`` is added.
See also the `Annex: Clocks Resolution in Python`_.
New nanosecond flavor of these functions may be added later, if a
concrete use case comes in.
New nanosecond flavor of these functions may be added later if an
operating system adds a new function provided better resolution.
Alternatives and discussion
@ -289,12 +297,13 @@ the Python standard library.
New time_ns module
------------------
Add a new ``time_ns`` module which contains the five new functions:
Add a new ``time_ns`` module which contains the six new functions:
* ``time_ns.clock_gettime(clock_id)``
* ``time_ns.clock_settime(clock_id, time: int)``
* ``time_ns.perf_counter()``
* ``time_ns.monotonic()``
* ``time_ns.perf_counter()``
* ``time_ns.process_time()``
* ``time_ns.time()``
The first question is if the ``time_ns`` should expose exactly the same
@ -314,8 +323,15 @@ get the ``time.ns.time()`` syntax.
Annex: Clocks Resolution in Python
==================================
Script ot measure the smallest difference between two ``time.time()`` and
``time.time_ns()`` reads ignoring differences of zero::
This annex contains the resolution of clocks measured in Python, and not
the resolution announced by the operating system or the resolution of
the internal structure used by the operating system.
Script
------
Example of script ot measure the smallest difference between two
``time.time()`` and ``time.time_ns()`` reads ignoring differences of zero::
import math
import time
@ -335,36 +351,82 @@ Script ot measure the smallest difference between two ``time.time()`` and
min_dt = min(filter(bool, min_dt))
print("min time() delta: %s ns" % math.ceil(min_dt * 1e9))
Results of time(), perf_counter() and monotonic().
Linux
-----
Linux (kernel 4.12 on Fedora 26):
* time_ns(): **84 ns**
* time(): **239 ns**
* perf_counter_ns(): 84 ns
* perf_counter(): 82 ns
* monotonic_ns(): 84 ns
* monotonic(): 81 ns
==================== ==========
Function Resolution
==================== ==========
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
==================== ==========
Notes on resolutions:
* ``clock()`` frequency is ``CLOCKS_PER_SECOND`` which is 1,000,000 Hz
(1 MHz): resolution of 1 us.
* ``times()`` frequency is ``os.sysconf("SC_CLK_TCK")`` (or the ``HZ``
constant) which is equal to 100 Hz: resolution of 10 ms.
* ``resource.getrusage()``, ``os.wait3()`` and ``os.wait4()`` use the
``ru_usage`` structure. The type of the ``ru_usage.ru_utime`` and
``ru_usage.ru_stime`` fields is the ``timeval`` structure which has a
resolution of 1 us.
Windows
-------
Windows 8.1:
* time_ns(): **318000 ns**
* time(): **894070 ns**
* perf_counter_ns(): 100 ns
* perf_counter(): 100 ns
* monotonic_ns(): 15000000 ns
* monotonic(): 15000000 ns
================= =============
Function Resolution
================= =============
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**
================= =============
The difference on ``time.time()`` is significant: **84 ns (2.8x better)
vs 239 ns on Linux and 318 us (2.8x better) vs 894 us on Windows**. The
difference (presion loss) will be larger next years since every day adds
864,00,000,000,000 nanoseconds to the system clock.
The frequency of ``perf_counter()`` and ``perf_counter_ns()`` comes from
``QueryPerformanceFrequency()``. The frequency is usually 10 MHz: resolution of
100 ns. In old Windows versions, the frequency was sometimes 3,579,545 Hz (3.6
MHz): resolution of 279 ns.
The difference on ``time.perf_counter()`` and ``time.monotonic clock()``
is not visible in this quick script since the script runs less than 1
Analysis
--------
The resolution of ``time.time_ns()`` is much better than
``time.time()``: **84 ns (2.8x better) vs 239 ns on Linux and 318 us
(2.8x better) vs 894 us on Windows**. The ``time.time()`` resolution will
becomes larger (worse) next years since every day adds
864,00,000,000,000 nanoseconds to the system clock which increases the
precision loss.
The difference between ``time.perf_counter()``, ``time.monotonic
clock()``, ``time.process_time()`` and their nanosecond variant is
not visible in this quick script since the script runs less than 1
minute, and the uptime of the computer used to run the script was
smaller than 1 week. A significant difference should be seen with an
uptime of 104 days or greater.
uptime of at least 104 days.
``resource.getrusage()`` and ``times()`` have a resolution greater or
equal to 1 microsecond, and so don't need a variant with nanosecond
resolution.
.. note::
Internally, Python starts ``monotonic()`` and ``perf_counter()``