Skip to content

Commit 5af25ce

Browse files
authored
[mypyc] Add experimental librt.time module with time() (#20723)
I've seen many performance-critical functions that have calls to `time.time()`, which is perhaps not that surprising. I don't want to just add a primitive for `time.time()`, since it's often monkey patched, and primitive functions generally can't be monkey patched. Instead, I add the `librt.time` module here, which has an efficient `time()` function that can be used in performance-critical code (but it can't be monkey patched). In a microbenchmark this was up to 70% faster than using `time.time()`. I used a lot of coding agent assist. I'm relying on CI to test the Windows implementation.
1 parent 606973f commit 5af25ce

File tree

13 files changed

+317
-1
lines changed

13 files changed

+317
-1
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
def time() -> float: ...

mypyc/build.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ class ModDesc(NamedTuple):
120120
["vecs/librt_vecs.h", "vecs/vec_template.c"],
121121
["vecs"],
122122
),
123+
ModDesc("librt.time", ["time/librt_time.c"], ["time/librt_time.h"], []),
123124
]
124125

125126
try:

mypyc/codegen/emitmodule.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
short_id_from_name,
5757
)
5858
from mypyc.errors import Errors
59-
from mypyc.ir.deps import LIBRT_BASE64, LIBRT_STRINGS, SourceDep
59+
from mypyc.ir.deps import LIBRT_BASE64, LIBRT_STRINGS, LIBRT_TIME, SourceDep
6060
from mypyc.ir.func_ir import FuncIR
6161
from mypyc.ir.module_ir import ModuleIR, ModuleIRs, deserialize_modules
6262
from mypyc.ir.ops import DeserMaps, LoadLiteral
@@ -632,6 +632,8 @@ def generate_c_for_modules(self) -> list[tuple[str, str]]:
632632
ext_declarations.emit_line("#include <base64/librt_base64.h>")
633633
if any(LIBRT_STRINGS in mod.dependencies for mod in self.modules.values()):
634634
ext_declarations.emit_line("#include <strings/librt_strings.h>")
635+
if any(LIBRT_TIME in mod.dependencies for mod in self.modules.values()):
636+
ext_declarations.emit_line("#include <time/librt_time.h>")
635637
# Include headers for conditional source files
636638
source_deps = collect_source_dependencies(self.modules)
637639
for source_dep in sorted(source_deps, key=lambda d: d.path):
@@ -1102,6 +1104,10 @@ def emit_module_exec_func(
11021104
emitter.emit_line("if (import_librt_strings() < 0) {")
11031105
emitter.emit_line("return -1;")
11041106
emitter.emit_line("}")
1107+
if LIBRT_TIME in module.dependencies:
1108+
emitter.emit_line("if (import_librt_time() < 0) {")
1109+
emitter.emit_line("return -1;")
1110+
emitter.emit_line("}")
11051111
emitter.emit_line("PyObject* modname = NULL;")
11061112
if self.multi_phase_init:
11071113
emitter.emit_line(f"{module_static} = module;")

mypyc/ir/deps.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ def get_header(self) -> str:
4949
LIBRT_STRINGS: Final = Capsule("librt.strings")
5050
LIBRT_BASE64: Final = Capsule("librt.base64")
5151
LIBRT_VECS: Final = Capsule("librt.vecs")
52+
LIBRT_TIME: Final = Capsule("librt.time")
5253

5354
BYTES_EXTRA_OPS: Final = SourceDep("bytes_extra_ops.c")
5455
BYTES_WRITER_EXTRA_OPS: Final = SourceDep("byteswriter_extra_ops.c")

mypyc/lib-rt/setup.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,5 +148,8 @@ def run(self) -> None:
148148
include_dirs=[".", "vecs"],
149149
extra_compile_args=cflags,
150150
),
151+
Extension(
152+
"librt.time", ["time/librt_time.c"], include_dirs=["."], extra_compile_args=cflags
153+
),
151154
]
152155
)

mypyc/lib-rt/time/librt_time.c

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
#define PY_SSIZE_T_CLEAN
2+
#include <Python.h>
3+
#include <time.h>
4+
#include <stdint.h>
5+
#include "librt_time.h"
6+
#include "pythoncapi_compat.h"
7+
#include "mypyc_util.h"
8+
9+
#ifdef _WIN32
10+
#include <windows.h>
11+
#else
12+
#include <sys/time.h>
13+
#endif
14+
15+
#ifdef MYPYC_EXPERIMENTAL
16+
17+
// Internal function that returns a C double for mypyc primitives
18+
// Returns high-precision time in seconds (like time.time())
19+
static double
20+
time_time_internal(void) {
21+
#ifdef _WIN32
22+
// Windows: Use GetSystemTimePreciseAsFileTime for ~100ns precision
23+
FILETIME ft;
24+
ULARGE_INTEGER large;
25+
26+
GetSystemTimePreciseAsFileTime(&ft);
27+
large.LowPart = ft.dwLowDateTime;
28+
large.HighPart = ft.dwHighDateTime;
29+
30+
// Windows FILETIME is 100-nanosecond intervals since January 1, 1601
31+
// 116444736000000000 = number of 100-ns intervals between 1601 and 1970
32+
// Convert directly to seconds: 100ns * 1e-9 = 1e-7
33+
int64_t intervals = large.QuadPart - 116444736000000000LL;
34+
return (double)intervals * 1e-7;
35+
36+
#else // Unix-like systems (Linux, macOS, BSD, etc.)
37+
38+
// Try clock_gettime(CLOCK_REALTIME) for nanosecond precision
39+
// This is available on POSIX.1-2001 and later (widely available on modern systems)
40+
#if defined(_POSIX_TIMERS) && _POSIX_TIMERS > 0
41+
struct timespec ts;
42+
if (clock_gettime(CLOCK_REALTIME, &ts) == 0) {
43+
// Convert seconds and nanoseconds separately to avoid large integer operations
44+
return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9;
45+
}
46+
// Fall through to gettimeofday if clock_gettime failed
47+
#endif
48+
49+
// Fallback: gettimeofday for microsecond precision
50+
// This is widely available (POSIX.1-2001, BSD, etc.)
51+
struct timeval tv;
52+
if (unlikely(gettimeofday(&tv, NULL) != 0)) {
53+
PyErr_SetFromErrno(PyExc_OSError);
54+
return CPY_FLOAT_ERROR;
55+
}
56+
57+
// Convert seconds and microseconds separately to avoid large integer operations
58+
return (double)tv.tv_sec + (double)tv.tv_usec * 1e-6;
59+
#endif
60+
}
61+
62+
// Wrapper function for normal Python extension usage
63+
static PyObject*
64+
time_time(PyObject *self, PyObject *const *args, size_t nargs) {
65+
if (nargs != 0) {
66+
PyErr_SetString(PyExc_TypeError, "time() takes no arguments");
67+
return NULL;
68+
}
69+
70+
double result = time_time_internal();
71+
if (result == CPY_FLOAT_ERROR) {
72+
return NULL;
73+
}
74+
return PyFloat_FromDouble(result);
75+
}
76+
77+
#endif
78+
79+
static PyMethodDef librt_time_module_methods[] = {
80+
#ifdef MYPYC_EXPERIMENTAL
81+
{"time", (PyCFunction)time_time, METH_FASTCALL,
82+
PyDoc_STR("Return the current time in seconds since the Unix epoch as a floating point number.")},
83+
#endif
84+
{NULL, NULL, 0, NULL}
85+
};
86+
87+
#ifdef MYPYC_EXPERIMENTAL
88+
89+
static int
90+
time_abi_version(void) {
91+
return LIBRT_TIME_ABI_VERSION;
92+
}
93+
94+
static int
95+
time_api_version(void) {
96+
return LIBRT_TIME_API_VERSION;
97+
}
98+
99+
#endif
100+
101+
static int
102+
librt_time_module_exec(PyObject *m)
103+
{
104+
#ifdef MYPYC_EXPERIMENTAL
105+
// Export mypyc internal C API via capsule
106+
static void *time_api[LIBRT_TIME_API_LEN] = {
107+
(void *)time_abi_version,
108+
(void *)time_api_version,
109+
(void *)time_time_internal,
110+
};
111+
PyObject *c_api_object = PyCapsule_New((void *)time_api, "librt.time._C_API", NULL);
112+
if (PyModule_Add(m, "_C_API", c_api_object) < 0) {
113+
return -1;
114+
}
115+
#endif
116+
return 0;
117+
}
118+
119+
static PyModuleDef_Slot librt_time_module_slots[] = {
120+
{Py_mod_exec, librt_time_module_exec},
121+
#ifdef Py_MOD_GIL_NOT_USED
122+
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
123+
#endif
124+
{0, NULL}
125+
};
126+
127+
static PyModuleDef librt_time_module = {
128+
.m_base = PyModuleDef_HEAD_INIT,
129+
.m_name = "time",
130+
.m_doc = "Fast time() function optimized for mypyc",
131+
.m_size = 0,
132+
.m_methods = librt_time_module_methods,
133+
.m_slots = librt_time_module_slots,
134+
};
135+
136+
PyMODINIT_FUNC
137+
PyInit_time(void)
138+
{
139+
return PyModuleDef_Init(&librt_time_module);
140+
}

mypyc/lib-rt/time/librt_time.h

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#ifndef LIBRT_TIME_H
2+
#define LIBRT_TIME_H
3+
4+
#ifndef MYPYC_EXPERIMENTAL
5+
6+
static int
7+
import_librt_time(void)
8+
{
9+
// All librt.time features are experimental for now, so don't set up the API here
10+
return 0;
11+
}
12+
13+
#else // MYPYC_EXPERIMENTAL
14+
15+
#include <Python.h>
16+
17+
#define LIBRT_TIME_ABI_VERSION 1
18+
#define LIBRT_TIME_API_VERSION 1
19+
#define LIBRT_TIME_API_LEN 3
20+
21+
static void *LibRTTime_API[LIBRT_TIME_API_LEN];
22+
23+
#define LibRTTime_ABIVersion (*(int (*)(void)) LibRTTime_API[0])
24+
#define LibRTTime_APIVersion (*(int (*)(void)) LibRTTime_API[1])
25+
#define LibRTTime_time (*(double (*)(void)) LibRTTime_API[2])
26+
27+
static int
28+
import_librt_time(void)
29+
{
30+
PyObject *mod = PyImport_ImportModule("librt.time");
31+
if (mod == NULL)
32+
return -1;
33+
Py_DECREF(mod); // we import just for the side effect of making the below work.
34+
void *capsule = PyCapsule_Import("librt.time._C_API", 0);
35+
if (capsule == NULL)
36+
return -1;
37+
memcpy(LibRTTime_API, capsule, sizeof(LibRTTime_API));
38+
if (LibRTTime_ABIVersion() != LIBRT_TIME_ABI_VERSION) {
39+
char err[128];
40+
snprintf(err, sizeof(err), "ABI version conflict for librt.time, expected %d, found %d",
41+
LIBRT_TIME_ABI_VERSION,
42+
LibRTTime_ABIVersion()
43+
);
44+
PyErr_SetString(PyExc_ValueError, err);
45+
return -1;
46+
}
47+
if (LibRTTime_APIVersion() < LIBRT_TIME_API_VERSION) {
48+
char err[128];
49+
snprintf(err, sizeof(err),
50+
"API version conflict for librt.time, expected %d or newer, found %d (hint: upgrade librt)",
51+
LIBRT_TIME_API_VERSION,
52+
LibRTTime_APIVersion()
53+
);
54+
PyErr_SetString(PyExc_ValueError, err);
55+
return -1;
56+
}
57+
return 0;
58+
}
59+
60+
#endif // MYPYC_EXPERIMENTAL
61+
62+
#endif // LIBRT_TIME_H

mypyc/primitives/librt_time_ops.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from mypyc.ir.deps import LIBRT_TIME
2+
from mypyc.ir.ops import ERR_MAGIC_OVERLAPPING
3+
from mypyc.ir.rtypes import float_rprimitive
4+
from mypyc.primitives.registry import function_op
5+
6+
function_op(
7+
name="librt.time.time",
8+
arg_types=[],
9+
return_type=float_rprimitive,
10+
c_function_name="LibRTTime_time",
11+
error_kind=ERR_MAGIC_OVERLAPPING,
12+
experimental=True,
13+
dependencies=[LIBRT_TIME],
14+
)

mypyc/primitives/registry.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,7 @@ def load_address_op(name: str, type: RType, src: str) -> LoadAddressDescription:
394394
import mypyc.primitives.float_ops
395395
import mypyc.primitives.int_ops
396396
import mypyc.primitives.librt_strings_ops
397+
import mypyc.primitives.librt_time_ops
397398
import mypyc.primitives.list_ops
398399
import mypyc.primitives.misc_ops
399400
import mypyc.primitives.str_ops

mypyc/test-data/irbuild-time.test

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
[case testTime_experimental]
2+
from librt.time import time
3+
import librt.time
4+
5+
def t1() -> float:
6+
return time()
7+
8+
def t2() -> float:
9+
return librt.time.time()
10+
[out]
11+
def t1():
12+
r0 :: float
13+
L0:
14+
r0 = LibRTTime_time()
15+
return r0
16+
def t2():
17+
r0 :: float
18+
L0:
19+
r0 = LibRTTime_time()
20+
return r0
21+
22+
[case testTimeExperimentalDisabled]
23+
from librt.time import time
24+
25+
def get_time() -> float:
26+
return time()
27+
[out]
28+
def get_time():
29+
r0 :: dict
30+
r1 :: str
31+
r2, r3 :: object
32+
r4 :: float
33+
L0:
34+
r0 = __main__.globals :: static
35+
r1 = 'time'
36+
r2 = CPyDict_GetItem(r0, r1)
37+
r3 = PyObject_Vectorcall(r2, 0, 0, 0)
38+
r4 = unbox(float, r3)
39+
return r4

0 commit comments

Comments
 (0)