Skip to content

Commit d67c2a7

Browse files
committed
Add sys._jit
1 parent 0119791 commit d67c2a7

File tree

13 files changed

+308
-61
lines changed

13 files changed

+308
-61
lines changed

Doc/library/sys.rst

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1282,6 +1282,59 @@ always available. Unless explicitly noted otherwise, all variables are read-only
12821282

12831283
.. versionadded:: 3.5
12841284

1285+
.. data:: _jit
1286+
1287+
Utilities for observing just-in-time compilation.
1288+
1289+
.. impl-detail::
1290+
1291+
JIT compilation is an *experimental implementation detail* of CPython.
1292+
``sys._jit`` is not guaranteed to exist or behave the same way in all
1293+
Python implementations, versions, or build configurations.
1294+
1295+
.. versionadded:: 3.14
1296+
1297+
.. function:: sys._jit.is_available()
1298+
1299+
Return ``True`` if the current Python executable supports JIT compilation,
1300+
and ``False`` otherwise. This can be controlled by building CPython with
1301+
the ``--experimental-jit`` option on Windows, and the
1302+
:option:`--enable-experimental-jit` option on all other platforms.
1303+
1304+
.. function:: sys._jit.is_enabled()
1305+
1306+
Return ``True`` if JIT compilation is enabled for the current Python
1307+
process (implies ``sys._jit.is_available()``), and ``False`` otherwise.
1308+
If JIT compilation is available, this can be controlled by setting the
1309+
:envvar:`PYTHON_JIT` environment variable to ``0`` (disabled) or ``1``
1310+
(enabled) at interpreter startup.
1311+
1312+
.. function:: sys._jit.is_active()
1313+
1314+
Return ``True`` if the topmost Python frame is currently executing JIT
1315+
code, and ``False`` otherwise.
1316+
1317+
.. note::
1318+
1319+
Due to the nature of tracing JIT compilers, repeated calls to this
1320+
function may give surprising results. For example, branching on its
1321+
return value will likely lead to unexpected behavior (if doing so
1322+
causes JIT code to be entered or exited):
1323+
1324+
.. code-block:: pycon
1325+
1326+
>>> for warmup in range(BIG_NUMBER):
1327+
... # This line is "hot", and is eventually JIT-compiled:
1328+
... if sys._jit.is_active():
1329+
... # This line is "cold", and is run in the interpreter:
1330+
... assert sys._jit.is_active()
1331+
...
1332+
Traceback (most recent call last):
1333+
File "<stdin>", line 5, in <module>
1334+
assert sys._jit.is_active()
1335+
~~~~~~~~~~~~~~~~~~^^
1336+
AssertionError
1337+
12851338
.. data:: last_exc
12861339

12871340
This variable is not always defined; it is set to the exception instance

Doc/using/cmdline.rst

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1279,7 +1279,13 @@ conflict.
12791279
free-threaded builds and to ``0`` otherwise. See :option:`-X
12801280
context_aware_warnings<-X>`.
12811281

1282-
.. versionadded:: 3.14
1282+
.. envvar:: PYTHON_JIT
1283+
1284+
On builds where experimental just-in-time compilation is available, this
1285+
variable can force the JIT to be disabled (``0``) or enabled (``1``) at
1286+
interpreter startup.
1287+
1288+
.. versionadded:: 3.13
12831289

12841290
Debug-mode variables
12851291
~~~~~~~~~~~~~~~~~~~~

Include/internal/pycore_interpframe.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ _PyFrame_Initialize(
146146
frame->return_offset = 0;
147147
frame->owner = FRAME_OWNED_BY_THREAD;
148148
frame->visited = 0;
149+
frame->jit_active = false;
149150
#ifdef Py_DEBUG
150151
frame->lltrace = 0;
151152
#endif
@@ -327,6 +328,7 @@ _PyFrame_PushTrampolineUnchecked(PyThreadState *tstate, PyCodeObject *code, int
327328
#endif
328329
frame->owner = FRAME_OWNED_BY_THREAD;
329330
frame->visited = 0;
331+
frame->jit_active = false;
330332
#ifdef Py_DEBUG
331333
frame->lltrace = 0;
332334
#endif

Include/internal/pycore_interpframe_structs.h

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,11 @@ struct _PyInterpreterFrame {
4343
#endif
4444
uint16_t return_offset; /* Only relevant during a function call */
4545
char owner;
46-
#ifdef Py_DEBUG
4746
uint8_t visited:1;
48-
uint8_t lltrace:7;
49-
#else
50-
uint8_t visited;
47+
// Set on interpreter entry frames when the JIT is active:
48+
uint8_t jit_active:1;
49+
#ifdef Py_DEBUG
50+
uint8_t lltrace:6;
5151
#endif
5252
/* Locals and stack */
5353
_PyStackRef localsplus[1];

Lib/test/libregrtest/utils.py

Lines changed: 4 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -335,43 +335,11 @@ def get_build_info():
335335
build.append('with_assert')
336336

337337
# --enable-experimental-jit
338-
tier2 = re.search('-D_Py_TIER2=([0-9]+)', cflags)
339-
if tier2:
340-
tier2 = int(tier2.group(1))
341-
342-
if not sys.flags.ignore_environment:
343-
PYTHON_JIT = os.environ.get('PYTHON_JIT', None)
344-
if PYTHON_JIT:
345-
PYTHON_JIT = (PYTHON_JIT != '0')
346-
else:
347-
PYTHON_JIT = None
348-
349-
if tier2 == 1: # =yes
350-
if PYTHON_JIT == False:
351-
jit = 'JIT=off'
352-
else:
353-
jit = 'JIT'
354-
elif tier2 == 3: # =yes-off
355-
if PYTHON_JIT:
356-
jit = 'JIT'
338+
if sys._jit.is_available():
339+
if sys._jit.is_enabled():
340+
build.append("JIT")
357341
else:
358-
jit = 'JIT=off'
359-
elif tier2 == 4: # =interpreter
360-
if PYTHON_JIT == False:
361-
jit = 'JIT-interpreter=off'
362-
else:
363-
jit = 'JIT-interpreter'
364-
elif tier2 == 6: # =interpreter-off (Secret option!)
365-
if PYTHON_JIT:
366-
jit = 'JIT-interpreter'
367-
else:
368-
jit = 'JIT-interpreter=off'
369-
elif '-D_Py_JIT' in cflags:
370-
jit = 'JIT'
371-
else:
372-
jit = None
373-
if jit:
374-
build.append(jit)
342+
build.append("JIT (disabled)")
375343

376344
# --enable-framework=name
377345
framework = sysconfig.get_config_var('PYTHONFRAMEWORK')

Lib/test/support/__init__.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2648,13 +2648,9 @@ def exceeds_recursion_limit():
26482648

26492649
Py_TRACE_REFS = hasattr(sys, 'getobjects')
26502650

2651-
try:
2652-
from _testinternalcapi import jit_enabled
2653-
except ImportError:
2654-
requires_jit_enabled = requires_jit_disabled = unittest.skip("requires _testinternalcapi")
2655-
else:
2656-
requires_jit_enabled = unittest.skipUnless(jit_enabled(), "requires JIT enabled")
2657-
requires_jit_disabled = unittest.skipIf(jit_enabled(), "requires JIT disabled")
2651+
_JIT_ENABLED = sys._jit.is_enabled()
2652+
requires_jit_enabled = unittest.skipUnless(_JIT_ENABLED, "requires JIT enabled")
2653+
requires_jit_disabled = unittest.skipIf(_JIT_ENABLED, "requires JIT disabled")
26582654

26592655

26602656
_BASE_COPY_SRC_DIR_IGNORED_NAMES = frozenset({

Lib/test/test_dis.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1336,7 +1336,7 @@ def test_loop_quicken(self):
13361336
# Loop can trigger a quicken where the loop is located
13371337
self.code_quicken(loop_test)
13381338
got = self.get_disassembly(loop_test, adaptive=True)
1339-
jit = import_helper.import_module("_testinternalcapi").jit_enabled()
1339+
jit = sys._jit.is_enabled()
13401340
expected = dis_loop_test_quickened_code.format("JIT" if jit else "NO_JIT")
13411341
self.do_disassembly_compare(got, expected)
13421342

Lib/test/test_sys.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2196,6 +2196,64 @@ def test_remote_exec_in_process_without_debug_fails_xoption(self):
21962196
self.assertIn(b"Remote debugging is not enabled", err)
21972197
self.assertEqual(out, b"")
21982198

2199+
class TestSysJIT(unittest.TestCase):
2200+
2201+
def test_jit_is_available(self):
2202+
available = sys._jit.is_available()
2203+
script = f"import sys; assert sys._jit.is_available() is {available}"
2204+
assert_python_ok("-c", script, PYTHON_JIT="0")
2205+
assert_python_ok("-c", script, PYTHON_JIT="1")
2206+
2207+
def test_jit_is_enabled(self):
2208+
available = sys._jit.is_available()
2209+
script = "import sys; assert sys._jit.is_enabled() is {enabled}"
2210+
assert_python_ok("-c", script.format(enabled=False), PYTHON_JIT="0")
2211+
assert_python_ok("-c", script.format(enabled=available), PYTHON_JIT="1")
2212+
2213+
def test_jit_is_active(self):
2214+
available = sys._jit.is_available()
2215+
script = textwrap.dedent(
2216+
"""
2217+
import _testinternalcapi
2218+
import operator
2219+
import sys
2220+
2221+
def frame_0_interpreter() -> None:
2222+
assert sys._jit.is_active() is False
2223+
2224+
def frame_1_interpreter() -> None:
2225+
assert sys._jit.is_active() is False
2226+
frame_0_interpreter()
2227+
assert sys._jit.is_active() is False
2228+
2229+
def frame_2_jit(expected: bool) -> None:
2230+
# Inlined into the last loop of frame_3_jit:
2231+
assert sys._jit.is_active() is expected
2232+
# Insert C frame:
2233+
operator.call(frame_1_interpreter)
2234+
assert sys._jit.is_active() is expected
2235+
2236+
def frame_3_jit() -> None:
2237+
# JITs just before the last loop:
2238+
for i in range(_testinternalcapi.TIER2_THRESHOLD + 1):
2239+
# Careful, doing this in the reverse order breaks tracing:
2240+
expected = {enabled} and i == _testinternalcapi.TIER2_THRESHOLD
2241+
assert sys._jit.is_active() is expected
2242+
frame_2_jit(expected)
2243+
assert sys._jit.is_active() is expected
2244+
2245+
def frame_4_interpreter() -> None:
2246+
assert sys._jit.is_active() is False
2247+
frame_3_jit()
2248+
assert sys._jit.is_active() is False
2249+
2250+
assert sys._jit.is_active() is False
2251+
frame_4_interpreter()
2252+
assert sys._jit.is_active() is False
2253+
"""
2254+
)
2255+
assert_python_ok("-c", script.format(enabled=False), PYTHON_JIT="0")
2256+
assert_python_ok("-c", script.format(enabled=available), PYTHON_JIT="1")
21992257

22002258

22012259
if __name__ == "__main__":

Modules/_testinternalcapi.c

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1165,12 +1165,6 @@ get_code_var_counts(PyObject *self, PyObject *_args, PyObject *_kwargs)
11651165
return NULL;
11661166
}
11671167

1168-
static PyObject *
1169-
jit_enabled(PyObject *self, PyObject *arg)
1170-
{
1171-
return PyBool_FromLong(_PyInterpreterState_GET()->jit);
1172-
}
1173-
11741168
#ifdef _Py_TIER2
11751169

11761170
static PyObject *
@@ -2093,13 +2087,10 @@ reset_rare_event_counters(PyObject *self, PyObject *Py_UNUSED(type))
20932087

20942088
interp->rare_events.set_class = 0;
20952089
interp->rare_events.set_bases = 0;
2096-
interp->rare_events.set_eval_frame_func = 0;
20972090
interp->rare_events.builtin_dict = 0;
20982091
interp->rare_events.func_modification = 0;
20992092

21002093
return Py_None;
2101-
}
2102-
21032094

21042095
#ifdef Py_GIL_DISABLED
21052096
static PyObject *
@@ -2293,7 +2284,6 @@ static PyMethodDef module_functions[] = {
22932284
{"get_co_localskinds", get_co_localskinds, METH_O, NULL},
22942285
{"get_code_var_counts", _PyCFunction_CAST(get_code_var_counts),
22952286
METH_VARARGS | METH_KEYWORDS, NULL},
2296-
{"jit_enabled", jit_enabled, METH_NOARGS, NULL},
22972287
#ifdef _Py_TIER2
22982288
{"add_executor_dependency", add_executor_dependency, METH_VARARGS, NULL},
22992289
{"invalidate_executors", invalidate_executors, METH_O, NULL},

Python/ceval.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1036,9 +1036,10 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int
10361036
entry_frame.f_executable = PyStackRef_None;
10371037
entry_frame.instr_ptr = (_Py_CODEUNIT *)_Py_INTERPRETER_TRAMPOLINE_INSTRUCTIONS + 1;
10381038
entry_frame.stackpointer = entry_frame.localsplus;
1039+
entry_frame.return_offset = 0;
10391040
entry_frame.owner = FRAME_OWNED_BY_INTERPRETER;
10401041
entry_frame.visited = 0;
1041-
entry_frame.return_offset = 0;
1042+
entry_frame.jit_active = false;
10421043
#ifdef Py_DEBUG
10431044
entry_frame.lltrace = 0;
10441045
#endif

0 commit comments

Comments
 (0)