PEP 564 – Neue Zeitfunktionen mit Nanosekundenauflösung
- Autor:
- Victor Stinner <vstinner at python.org>
- Status:
- Final
- Typ:
- Standards Track
- Erstellt:
- 16. Okt. 2017
- Python-Version:
- 3.7
- Resolution:
- Python-Dev Nachricht
Inhaltsverzeichnis
- Zusammenfassung
- Begründung
- Änderungen
- Alternativen und Diskussion
- Anhang: Taktfrequenzen in Python
- Links
- Urheberrecht
Zusammenfassung
Fügt sechs neue "Nanosekunden"-Varianten bestehender Funktionen zum Modul time hinzu: clock_gettime_ns(), clock_settime_ns(), monotonic_ns(), perf_counter_ns(), process_time_ns() und time_ns(). Ähnlich wie die bestehenden Funktionen ohne den Suffix _ns bieten sie Nanosekundenauflösung: Sie geben eine Anzahl von Nanosekunden als Python int zurück.
Die Auflösung von time.time_ns() ist auf Linux und Windows 3-mal besser als die Auflösung von time.time().
Begründung
Gleitkommatyp auf 104 Tage beschränkt
Die Taktfrequenzen von Desktop- und Laptop-Computern nähern sich immer mehr der Nanosekundenauflösung an. Immer mehr Takte haben eine Frequenz im MHz-Bereich, bis hin zu GHz für den CPU TSC-Takt.
Die Python-Funktion time.time() gibt die aktuelle Zeit als Gleitkommazahl zurück, die normalerweise eine 64-Bit-Binär-Gleitkommazahl (im IEEE 754-Format) ist.
Das Problem ist, dass der float-Typ nach 104 Tagen beginnt, Nanosekunden zu verlieren. Umwandlung von Nanosekunden (int) in Sekunden (float) und dann zurück in Nanosekunden (int), um zu prüfen, ob Konvertierungen Präzision verlieren
# 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() gibt die seit der UNIX-Epoche vergangenen Sekunden zurück: 1. Januar 1970. Diese Funktion hat seit Mai 1970 (vor 47 Jahren) keine Nanosekundenpräzision mehr.
>>> 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
Zuvor abgelehnte PEP
Vor fünf Jahren schlug PEP 410 eine große und komplexe Änderung aller Python-Funktionen vor, die Zeit zurückgeben, um die Nanosekundenauflösung unter Verwendung des Typs decimal.Decimal zu unterstützen.
Die PEP wurde aus verschiedenen Gründen abgelehnt
- Die Idee, einen neuen optionalen Parameter hinzuzufügen, um den Ergebnistyp zu ändern, wurde abgelehnt. Dies ist eine ungewöhnliche (und schlechte?) Programmierpraxis in Python.
- Es war nicht klar, ob Hardware-Takte wirklich eine Auflösung von 1 Nanosekunde hatten oder ob das auf Python-Ebene sinnvoll war.
- Der Typ
decimal.Decimalist in Python ungewöhnlich und erfordert daher die Anpassung des Codes, um ihn zu verarbeiten.
Probleme durch Präzisionsverlust
Beispiel 1: Zeitdelta in langlaufendem Prozess messen
Ein Server läuft länger als 104 Tage. Ein Takt wird vor und nach dem Ausführen einer Funktion gelesen, um deren Leistung zu messen und Leistungsprobleme zur Laufzeit zu erkennen. Ein solches Benchmark verliert nur durch den von Takten verwendeten Gleitkommatyp Präzision, nicht durch die Taktfrequenz.
Bei Python-Mikrobenchmarks werden häufig Funktionsaufrufe beobachtet, die weniger als 100 ns dauern. Ein Unterschied von wenigen Nanosekunden kann signifikant werden.
Beispiel 2: Zeiten mit unterschiedlicher Auflösung vergleichen
Zwei Programme „A“ und „B“ laufen auf demselben System und verwenden den Systemtakt. Programm A liest den Systemtakt mit Nanosekundenauflösung und schreibt einen Zeitstempel mit Nanosekundenauflösung. Programm B liest den Zeitstempel mit Nanosekundenauflösung, vergleicht ihn aber mit dem Systemtakt, der mit schlechterer Auflösung gelesen wird. Um das Beispiel zu vereinfachen, sagen wir, B liest den Takt mit Sekundenauflösung. In diesem Fall gibt es ein Zeitfenster von 1 Sekunde, in dem Programm B den von A geschriebenen Zeitstempel als „in der Zukunft“ sehen kann.
Heutzutage unterstützen immer mehr Datenbanken und Dateisysteme die Speicherung von Zeiten mit Nanosekundenauflösung.
Hinweis
Dieses Problem wurde bereits für die Änderungszeit von Dateien behoben, indem das Feld st_mtime_ns zum Ergebnis von os.stat() hinzugefügt wurde, und indem Nanosekunden in os.utime() akzeptiert wurden. Diese PEP schlägt vor, die Korrektur zu verallgemeinern.
CPython-Erweiterungen der letzten 5 Jahre
Da die PEP 410 abgelehnt wurde
- Die Struktur
os.stat_resulterhielt 3 neue Felder für Zeitstempel als Nanosekunden (Pythonint):st_atime_ns,st_ctime_nsundst_mtime_ns. - Die PEP 418 wurde angenommen, Python 3.3 erhielt 3 neue Takte:
time.monotonic(),time.perf_counter()undtime.process_time(). - Die CPython-private „pytime“-C-API für die Zeitverwaltung verwendet nun einen neuen Typ
_PyTime_t: eine einfache 64-Bit-Ganzzahl (Cint64_t). Die Einheit von_PyTime_tist ein Implementierungsdetail und nicht Teil der API. Die Einheit ist derzeit1 Nanosekunde.
Bestehende Python-APIs, die Nanosekunden als Ganzzahl verwenden
Die Struktur os.stat_result hat 3 Felder für Zeitstempel als Nanosekunden (int): st_atime_ns, st_ctime_ns und st_mtime_ns.
Der Parameter ns der Funktion os.utime() akzeptiert ein Tupel (atime_ns: int, mtime_ns: int): Nanosekunden.
Änderungen
Neue Funktionen
Diese PEP fügt dem Modul time sechs neue Funktionen hinzu
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()
Diese Funktionen sind ähnlich wie die Version ohne den Suffix _ns, geben aber eine Anzahl von Nanosekunden als Python int zurück.
Zum Beispiel gilt time.monotonic_ns() == int(time.monotonic() * 1e9), wenn der Wert von monotonic() klein genug ist, um keine Präzision zu verlieren.
Diese Funktionen sind erforderlich, da sie "große" Zeitstempel zurückgeben können, wie time.time(), das die UNIX-Epoche als Referenz verwendet, und daher sind ihre float-zurückgebenden Varianten wahrscheinlich, bei Nanosekundenauflösung Präzision zu verlieren.
Unveränderte Funktionen
Da die Funktion time.clock() in Python 3.3 als veraltet markiert wurde, wird keine time.clock_ns() hinzugefügt.
Python hat andere zeitbezogene Funktionen. Für diese anderen Funktionen wird keine Nanosekundenvariante vorgeschlagen, entweder weil ihre interne Auflösung größer oder gleich 1 us ist, oder weil ihr Maximalwert klein genug ist, um keine Präzision zu verlieren. Zum Beispiel sollte die maximale Auflösung von time.clock_getres() 1 Sekunde betragen.
Beispiele für unveränderte Funktionen
os-Modul:sched_rr_get_interval(),times(),wait3()undwait4()resource-Modul:ru_utimeundru_stimeFelder vongetrusage()signal-Modul:getitimer(),setitimer()time-Modul:clock_getres()
Siehe auch den Anhang: Taktfrequenzen in Python.
Eine neue Nanosekunden-zurückgebende Variante dieser Funktionen könnte später hinzugefügt werden, wenn ein Betriebssystem neue Funktionen mit besserer Auflösung bereitstellt.
Alternativen und Diskussion
Sub-Nanosekundenauflösung
time.time_ns() API ist nicht theoretisch zukunftssicher: Wenn die Taktfrequenzen unterhalb der Nanosekunde weiter steigen, könnten neue Python-Funktionen erforderlich sein.
In der Praxis reicht die Auflösung von 1 Nanosekunde derzeit für alle Strukturen aus, die von allen gängigen Betriebssystemfunktionen zurückgegeben werden.
Hardware-Takte mit einer Auflösung besser als 1 Nanosekunde existieren bereits. Zum Beispiel ist die Frequenz eines CPU TSC-Takts die Basisfrequenz der CPU: Die Auflösung beträgt etwa 0,3 ns für eine CPU mit 3 GHz. Benutzer, die Zugriff auf solche Hardware haben und wirklich Sub-Nanosekundenauflösung benötigen, können Python jedoch für ihre Bedürfnisse erweitern. Ein solch seltener Anwendungsfall rechtfertigt nicht, die Python-Standardbibliothek so zu gestalten, dass sie Sub-Nanosekundenauflösung unterstützt.
Für die CPython-Implementierung ist die Nanosekundenauflösung praktisch: Der Standard und gut unterstützte Typ int64_t kann verwendet werden, um einen nanosekundenpräzisen Zeitstempel zu speichern. Er unterstützt einen Zeitbereich von -292 Jahren bis +292 Jahren. Mit der UNIX-Epoche als Referenz unterstützt er daher die Darstellung von Zeiten von 1677 bis 2262.
>>> 1970 - 2 ** 63 / (10 ** 9 * 3600 * 24 * 365.25)
1677.728976954687
>>> 1970 + 2 ** 63 / (10 ** 9 * 3600 * 24 * 365.25)
2262.271023045313
Änderung des Ergebnistyps von time.time()
Es wurde vorgeschlagen, time.time() so zu ändern, dass es einen anderen Zahlentyp mit besserer Präzision zurückgibt.
Die PEP 410 schlug die Rückgabe von decimal.Decimal vor, das bereits existiert und beliebige Präzision unterstützt, aber es wurde abgelehnt. Abgesehen von decimal.Decimal ist derzeit kein portabler reeller Zahlentyp mit besserer Präzision in Python verfügbar.
Die Änderung des eingebauten Python-Typs float liegt außerhalb des Geltungsbereichs dieser PEP.
Darüber hinaus birgt die Änderung bestehender Funktionen zur Rückgabe eines neuen Typs das Risiko von Brüchen in der Abwärtskompatibilität, selbst wenn der neue Typ sorgfältig konzipiert ist.
Unterschiedliche Typen
Viele Ideen für neue Typen wurden vorgeschlagen, um größere oder beliebige Präzision zu unterstützen: Brüche, Strukturen oder 2-Tupel mit Ganzzahlen, Festkommazahlen usw.
Siehe auch PEP 410 für eine frühere lange Diskussion über andere Typen.
Das Hinzufügen eines neuen Typs erfordert mehr Aufwand zur Unterstützung als die Wiederverwendung des vorhandenen Typs int. Die Standardbibliothek, Drittanbieter-Code und Anwendungen müssten angepasst werden, um ihn zu unterstützen.
Der Python-Typ int ist gut bekannt, gut unterstützt, einfach zu handhaben und unterstützt alle arithmetischen Operationen wie dt = t2 - t1.
Darüber hinaus ist die Übernahme/Rückgabe einer Ganzzahl von Nanosekunden kein neues Konzept in Python, wie os.stat_result und os.utime(ns=(atime_ns, mtime_ns)) belegen.
Hinweis
Wenn der Python-Typ float größer wird (z. B. decimal128 oder float128), erhöht sich auch die Präzision von time.time().
Unterschiedliche API
Die API time.time(ns=False) wurde vorgeschlagen, um die Hinzufügung neuer Funktionen zu vermeiden. Es ist eine ungewöhnliche (und schlechte?) Programmierpraxis in Python, den Ergebnistyp abhängig von einem Parameter zu ändern.
Verschiedene Optionen wurden vorgeschlagen, um dem Benutzer die Wahl der Zeitauflösung zu ermöglichen. Wenn jedes Python-Modul eine andere Auflösung verwendet, kann es schwierig werden, verschiedene Auflösungen zu handhaben, anstatt nur Sekunden (time.time() gibt float zurück) und Nanosekunden (time.time_ns() gibt int zurück). Darüber hinaus gibt es, wie oben erwähnt, in der Praxis keine Notwendigkeit für eine Auflösung besser als 1 Nanosekunde in der Python-Standardbibliothek.
Ein neues Modul
Es wurde vorgeschlagen, ein neues Modul time_ns mit den folgenden Funktionen hinzuzufügen
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()
Die erste Frage ist, ob das Modul time_ns genau die gleiche API (Konstanten, Funktionen usw.) wie das Modul time bereitstellen soll. Es kann schmerzhaft sein, zwei Varianten des time-Moduls zu pflegen. Wie sollen Benutzer eine Wahl zwischen diesen beiden Modulen treffen?
Wenn morgen weitere Nanosekundenvarianten im Modul os benötigt werden, müssen wir dann auch ein neues Modul os_ns hinzufügen? Es gibt Funktionen im Zusammenhang mit Zeit in vielen Modulen: time, os, signal, resource, select usw.
Eine andere Idee ist, ein Untermodul time.ns oder einen verschachtelten Namespace hinzuzufügen, um die Syntax time.ns.time() zu erhalten, aber dies leidet unter den gleichen Problemen.
Anhang: Taktfrequenzen in Python
Dieser Anhang enthält die Auflösung von Takten, wie sie in Python gemessen wird, und nicht die vom Betriebssystem angekündigte Auflösung oder die Auflösung der internen Struktur, die vom Betriebssystem verwendet wird.
Skript
Beispiel für ein Skript zur Messung des kleinsten Unterschieds zwischen zwei Lesevorgängen von time.time() und time.time_ns(), wobei Nullunterschiede ignoriert werden
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
In Python gemessene Taktfrequenzen unter Fedora 26 (Kernel 4.12)
| Funktion | Entscheidung |
|---|---|
| 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 |
Hinweise zur Auflösung
- Die Frequenz von
clock()istCLOCKS_PER_SECOND, was 1.000.000 Hz (1 MHz) entspricht: Auflösung von 1 us. - Die Frequenz von
times()istos.sysconf("SC_CLK_TCK")(oder die KonstanteHZ), was 100 Hz entspricht: Auflösung von 10 ms. resource.getrusage(),os.wait3()undos.wait4()verwenden die Strukturru_usage. Der Typ der Felderru_usage.ru_utimeundru_usage.ru_stimeist die Strukturtimeval, die eine Auflösung von 1 us hat.
Windows
In Python gemessene Taktfrequenzen unter Windows 8.1
| Funktion | Entscheidung |
|---|---|
| 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 |
Die Frequenz von perf_counter() und perf_counter_ns() stammt von QueryPerformanceFrequency(). Die Frequenz beträgt normalerweise 10 MHz: Auflösung von 100 ns. In älteren Windows-Versionen betrug die Frequenz manchmal 3.579.545 Hz (3,6 MHz): Auflösung von 279 ns.
Analyse
Die Auflösung von time.time_ns() ist viel besser als die von time.time(): **84 ns (2,8x besser) vs. 239 ns unter Linux und 318 us (2,8x besser) vs. 894 us unter Windows**. Die Auflösung von time.time() wird nur größer (schlechter) mit jedem vergehenden Jahr, da jeden Tag 86.400.000.000.000 Nanosekunden zur Systemuhr hinzukommen, was den Präzisionsverlust erhöht.
Der Unterschied zwischen time.perf_counter(), time.monotonic(), time.process_time() und ihren jeweiligen Nanosekundenvarianten ist in diesem kurzen Skript nicht sichtbar, da das Skript weniger als 1 Minute läuft und die Uptime des Computers, auf dem das Skript ausgeführt wurde, kleiner als 1 Woche war. Ein signifikanter Unterschied kann auftreten, wenn die Uptime 104 Tage oder mehr erreicht.
resource.getrusage() und times() haben eine Auflösung von 1 Mikrosekunde oder mehr und benötigen daher keine Variante mit Nanosekundenauflösung.
Hinweis
Intern setzt Python auf einigen Plattformen die Takte monotonic() und perf_counter() auf Null, was indirekt den Präzisionsverlust reduziert.
Links
Urheberrecht
Dieses Dokument wurde gemeinfrei erklärt.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0564.rst
Zuletzt geändert: 2025-02-01 08:59:27 GMT